Skip to main content
skill CX Codex

debugging-network-issues — Claude Code skill: what it does

A methodology for network, streaming and protocol failures where the obvious cause is probably wrong.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add daymade/claude-code-skills --skill debugging-network-issues -a claude-code
Open on GitHub

What debugging-network-issues does

The skill slows down the reflex to diagnose from one log line. It asks for a concrete artifact first — a log entry, a pcap frame, probe output, a metric sample — and if none exists, you instrument or capture before going further. Hypotheses have to be falsifiable: name the observation that would make you drop the theory, then go look for it.

A triage table sits at the top and routes known symptoms to dedicated skills. Tailscale clashing with a proxy or VPN, certificate errors across every direct-routed domain, slow bulk transfers that change with the exit node — those belong to tunnel-doctor. Cloudflare config and Windows remote desktop quality have their own skills. When nothing matches, the general layered method applies.

One branch covers the case with no errors at all: every request returns 200, health checks look fine, transfers crawl. Go straight to the throughput-collapse step there, because the error-driven steps have nothing to bite on. The method comes out of a real five-hour production incident where stacked assumptions burned hours a short experiment would have saved.

What it helps with

  • trace an ECONNRESET or RST_STREAM with no clear cause
  • find where an SSE stream stalls mid-transfer
  • separate slow upload to origin from a stalled backend
  • test a hypothesis so a single observation can kill it
  • isolate a fault between client, CDN and reverse proxy

Example prompts for the agent

  • My SSE stream dies after 130 seconds, walk me through this layer by layer
  • Everything returns 200 but bulk transfers crawl — where do I start
  • Get me evidence before you name a cause for this ECONNRESET

How to install debugging-network-issues

1Skills CLI (Vercel)

npx skills add daymade/claude-code-skills --skill debugging-network-issues -a claude-code

2Manually: put the files in place

~/.claude/skills/debugging-network-issues/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/debugging-network-issues/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about debugging-network-issues

How do I install debugging-network-issues in Claude?

Run npx skills add daymade/claude-code-skills --skill debugging-network-issues -a claude-code. The skill lands at ~/.claude/skills/debugging-network-issues/SKILL.md.

What does debugging-network-issues do?

It runs a network and protocol investigation through layered experiments and falsification-first hypothesis testing. Triage comes first, since a known symptom may already have a dedicated skill.

When should I use tunnel-doctor instead?

When the symptom points at a known Tailscale, TUN, DNS or proxy-path conflict. Certificate errors on every direct-routed domain at once, or slow transfers that shift with the active exit node, also go to tunnel-doctor.

Source text

SKILL.md · MIT
---
name: debugging-network-issues
description: >-
  Investigates network, streaming and protocol failures with falsification-first layered
  experiments, the general fallback for unknown root causes. Use for ECONNRESET / RST_STREAM / SSE
  stalls, CDN/CGNAT timeouts or certificate errors, throughput collapse where everything returns 200
  but transfers crawl, or LAN identity/topology mysteries. Not for known
  Tailscale/TUN/DNS/proxy-path conflicts (use tunnel-doctor).
---

# Debugging Network Issues

Evidence-driven investigation methodology for incidents where the obvious cause is probably wrong. Built from a real 5-hour production case (see [references/case-sse-rst-130s.md](references/case-sse-rst-130s.md)) where assumption-stacking wasted hours that a 10-minute layered experiment would have resolved.

Apply this skill when the user reports a network/streaming/protocol symptom and the investigator feels tempted to diagnose from one log line or one circumstantial data point. The skill's job is to slow that reflex down.

## Triage first — is this a known domain?

Before applying the general methodology below, check whether the symptom points at a stack that already has a dedicated skill in this repo. Those carry the domain-specific symptom→cause→fix tables this skill deliberately stays general about — start there, and come back here for methodology if the root cause turns out to be elsewhere.

| If the symptom is…                                                                                                                                                                                       | Start with                                   |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| macOS Tailscale ⨯ proxy/VPN conflict (Shadowrocket / Clash / Surge): `tailscale ping` works but SSH/curl/git fails, `Connection closed by 198.18.x.x`, TUN DNS hijack, ~60s `getaddrinfo` resolver stall | **tunnel-doctor**                            |
| Cloudflare config: `ERR_TOO_MANY_REDIRECTS`, SSL-mode mismatch, DNS / proxy-status issues behind the orange cloud                                                                                        | **cloudflare-troubleshooting**               |
| Windows App / AVD / W365 RDP connection quality: WebSocket instead of UDP Shortpath, high RTT, STUN/TURN interference                                                                                    | **windows-remote-desktop-connection-doctor** |
| Client-side proxy / VPN / TUN misrouting: one specific site fails with `ERR_CONNECTION_CLOSED` or `SSL_ERROR_SYSCALL`, other sites work, DNS returns fake/TUN IPs, and adding a PROXY rule did not help | **this skill** — read [references/case-proxy-tun-cname-override.md](references/case-proxy-tun-cname-override.md) first |
| TLS certificate-verification errors (`UNKNOWN_CERTIFICATE_VERIFICATION_ERROR`, a cert for the wrong site) or mid-handshake EOFs on **every** DIRECT-routed/domestic domain at once, while proxied domains work — and any proxy health watchdog still reports green                                        | **tunnel-doctor** (TUN DIRECT split-brain step) |
| The proxy is reachable and the OS/Tailscale route is healthy, but bulk transfers are slow and changing the active exit/node changes the measured rate | **tunnel-doctor** — proxy node / exit / chain capacity branch |
| **Nothing errors — it is just slow.** Every request returns 200, latency and health checks look fine, but transfers crawl, bulk jobs overrun their estimates, or files arrive truncated | **this skill** — go straight to [Step 0.7](#step-07-throughput-collapse--when-nothing-errors-and-everything-is-slow); the error-driven steps have nothing to bite on |
| A whole group of smart-home / IoT devices is offline at once — often weeks after a router/AP swap or Wi-Fi settings change; the hub is "right there" but its children are all dead, plus some direct-Wi-Fi devices | **this skill** — read [references/case-iot-router-migration-wpa3.md](references/case-iot-router-migration-wpa3.md) first |

If none match — or you tried a domain skill and the evidence points elsewhere — continue below. The methodology generalizes to any multi-layer system.

> **Note for this skill specifically**: If the symptom is a Cloudflare 524/522 on a **large `POST` body** (e.g., `/<openrouter-path>` with `Content-Length` > 1 MB), the failure is often **upload time to origin exceeding Cloudflare's origin read timeout**, not backend slowness. Use the upload-vs-processing checklist below before assuming a backend stall.

## Core principles

### 1. Evidence over assumption

If you cannot point to a concrete artifact — log line, pcap frame, probe output, metric sample — you are guessing, not diagnosing. Before stating "X is the cause", require yourself to name the direct evidence. If it does not exist yet, add instrumentation (see [references/instrumentation-patterns.md](references/instrumentation-patterns.md)) or capture it (see [references/packet-capture-recipes.md](references/packet-capture-recipes.md)) before continuing.

### 2. Falsification over confirmation

N independent sources "confirming" a hypothesis does not make it true. One falsifying observation rules it out. Before acting on a hypothesis, answer:

> "What observation would make me abandon this hypothesis?"

If the answer is "nothing" or "I cannot think of one", the hypothesis is unfalsifiable and must not drive the investigation. If the answer is concrete, go look for that observation before committing to action.

### 3. Layered isolation

Multi-hop systems (client → CDN → LB → reverse proxy → app → upstream) concentrate bugs at the seams between layers. When a symptom could plausibly come from several layers, **do not reason about which layer; test**. Compare the same logical request across verified paths, name every changed hop, and attribute only the isolated difference. For intermittent failures, use a bounded return to the original condition before treating a successful variation as causal evidence. Read [references/layered-isolation-experiment.md](references/layered-isolation-experiment.md) for path design and A/B/A interpretation.

**The same technique isolates capacity, not just correctness.** When the symptom is a rate rather than an error, vary the *stack* instead of the *hop*: measure the same direction over two channels that share the network path but share no application code. It is the cheapest way to stop tuning an application that was never the bottleneck. Resist compressing the reading into "agreement means the path" — that holds only when both channels are *slow*; two fast channels mean the probe failed to reproduce the symptom and prove nothing at all. See Step 0.7 for how to read each of the four outcomes.

### 4. Counter-review before committing

Before committing to a root cause or shipping a fix, have independent reviewers challenge the conclusion — not confirm it. Agents are good at surfacing risks a single investigator did not think of; they are bad at weighing them. Apply the four-question filter (see [references/counter-review-pattern.md](references/counter-review-pattern.md)) to every finding before it shapes action.

### 5. A green health check certifies only the path it probes

Monitors watch the paths their authors thought to probe. Multi-plane systems — a TUN proxy with a DIRECT plane and a proxied plane, a service with a data plane and a control plane, a stack with a backend API and a web login page — fail one plane at a time, and a watchdog that probes only the other plane stays green through the entire outage. In the incident behind this principle, a proxy health daemon probed one overseas endpoint through the proxy every 5 minutes and logged "healthy" for 2+ hours while the direct plane was completely down.

Before accepting "the monitor says it's healthy" as evidence, ask: **which exact path does that check exercise?** Its green counts as evidence for that path alone. Enumerate the planes the system actually forwards or serves, and probe the failing one directly — the check that would have caught the outage is usually one curl away.

**A check also certifies only the *quantity* it is large enough to measure.** Path coverage is one axis; scale is the other, and it is the one that hides throughput collapse. A liveness probe returns a few hundred bytes, so its timing is dominated by handshake and round-trip — in the case study it answered in 40 ms all day, unchanged, across a 100× swing in the link's actual capacity, and it was not lying: it genuinely did what it measures. Same for `ping`/RTT, which times a small packet's round trip and says nothing about capacity.

The practical rule: **a probe measures the link only once transfer time dominates its total time.** So size it by the answer you need, not by convenience — if a probe returns in well under a second, essentially all of that was setup and you have measured setup. Sidestep the sizing question entirely by budgeting *time* instead of bytes (stream for N seconds, divide what arrived by N), which is what Step 0.7's commands do and why they stay cheap on a link that is already crawling. Extend the question to: **which path, and at what scale?**

### 6. Exhaust the instruments before asking the user — escalate only what only they can provide

A user's spoken observation ("the Wi-Fi drops for a few seconds", "some apps report connection errors") is a *lead*, not a question to hand back. Treat each open sub-question it contains as presumed observable, and search for the instrument before asking: monitor/probe ledgers (where a health daemon exists, its log holds the failure timetable of the paths it probes — in both directions: a green ledger never falsifies the user's observation (Principle 5), and a red one says nothing about paths it never probed), system logs (kernel link/disassociation events settle "did Wi-Fi physically drop"; per-process error bursts settle "which apps failed" only for apps that log there), and control channels on the same network (a second host over a different medium — a wired box with no proxy, a hotspot comparison, even the router's own status page; when no control channel exists, say so explicitly rather than treating the question as answered). When the instrument search comes back empty for a sub-question, the move is Step 4 — stand up instrumentation and wait for recurrence — not "ask the user to recall".

Only three categories legitimately reach the user mid-investigation:

1. **Decision rights** — production changes, restarts, spending, anything past your authorization boundary.
2. **Physical actions and first-person observations no instrument captured** — unplugging a box, reading a label password, whether the Wi-Fi icon itself disappeared on a device you have no telemetry for.
3. **Credentials or access you do not have** — a management password that exists only on a device label.

Observed 2026-09-10: a home-network investigation closed its report with four questions for the user. "Which apps reported errors" — its diagnostic content, a per-plane failure timetable — was already in the health daemon's ledger (the app-name list itself lives only on the user's screen, and adds no layer information the timetable lacks); "did Wi-Fi actually drop" was settled by twelve hours of kernel logs with zero disassociation events; both went to the user anyway. The fourth — "is the retired router still broadcasting" — is the honest counter-example: its ARP absence proved nothing (Trap 14: a probe certifies only the L2 domain it ran on), the platform redacted the Wi-Fi scan, so that one correctly reached the user as a five-second physical check.

For every question you hand the user, write one sentence in the report naming why no instrument can answer it and which of the three categories it falls in. If you cannot produce that sentence, the question is not exhausted — go back to the instruments; do not ask.

## Workflow

Copy this checklist into the investigation notes and check items off:

```
Investigation Progress:
- [ ] Step 0:   Scope the symptom (exact error, exact times, who, who-not, what changed)
- [ ] Step 0.5: Verify the premise — does direct evidence show the symptom is actually happening?
- [ ] Step 0.6: **For large POST bodies: distinguish upload-timeout from processing-timeout** (see recipe below)
- [ ] Step 0.7: **If nothing errors and it is just slow: measure a rate, then two-channel it** (see recipe below)
- [ ] Step 1:   Gather direct evidence at every hop before hypothesizing
- [ ] Step 2:   Frame ≥3 hypotheses; for each, name (a) what falsifies it, (b) which layer boundary the intervention would target
- [ ] Step 3:   Design a decisive experiment (for network: layered isolation)
- [ ] Step 4:   Add instrumentation if evidence gaps block direct observation
- [ ] Step 5:   Execute, record actual vs predicted
- [ ] Step 6:   Counter-review before acting
- [ ] Step 7:   Fix + re-run the same experiment to verify
- [ ] Step 8:   Document wrong turns as teaching material
```

### Step 0: Scope

A tight scope is the difference between a 20-minute investigation and a 5-hour one. Before looking at anything, extract:

- **Exact error string** (copy-paste, not paraphrase). `socket closed` is not the same as `ECONNRESET` is not the same as `HTTP/2 RST_STREAM INTERNAL_ERROR (err 2)`.
- **Exact timestamps** (ISO-8601 with timezone, not "yesterday evening")
- **Reproducibility** (every time / intermittent / only specific users)
- **Who is affected, who is not** (differential observations narrow the search)
- **What changed recently** (deploys, config, upstream dependencies, client versions)

Distinguish symptom from diagnosis. "Slow" is not a symptom. "Request took 130.898s then returned HTTP/2 INTERNAL_ERROR" is.

**But do not let that sentence route a real incident into the bin.** It demands quantification, not an error code — and a whole failure family quantifies as a *rate* with no error code anywhere: every request returns 200, nothing resets, and the system is still unusable. "Slow" becomes a symptom the moment you write it as **bytes per second over a stated payload size** ("8.4 MB in 95 s = 0.09 MB/s, HTTP 200 throughout"). Once you have that number, Step 0.7 has a decisive experiment for it. What stays out of scope is the *unmeasured* complaint — "users say it feels slow" — which is Step 0.5's problem, not this one.

### Step 0.5: Verify the premise

Before investing in a full investigation, confirm the reported symptom is actually happening — not just inferred from downstream effects or user frustration. One cheap direct observation beats hours spent investigating a non-problem.

Ask: **"What direct evidence shows this symptom is real?"**

- If the user reports "timeout at 130s": is that from a timestamped log, a browser network panel, or a recollection?
- If the user reports "connection reset": did they see the packet or is it inferred from a retry spike?
- If the user reports "fails for some but not others": has it been reproduced in a controlled test, or is it anecdotal?

Acceptable premises:

- Log line with timestamp and error string
- Browser DevTools Network screenshot showing the failure
- Reproduction command that shows the symptom on demand
- Metrics chart showing the specific error count rising

Not sufficient as premise:

- "Users are saying it feels slow"
- "The alert fired but I did not check what actually failed"
- "Last week someone mentioned..."

If the premise fails verification, the fix is observation — not investigation. Add the missing telemetry, wait for the next occurrence with instrumentation in place, and return when you have real data. Resist the sunk-cost instinct to investigate anyway "since we are already here".

### Step 0.6: Upload-timeout vs processing-timeout for large POST bodies

For CDN-fronted `POST`/`PUT` endpoints with large bodies, the most common misdiagnosis is blaming backend slowness when the real problem is **time-to-upload-body exceeding the CDN/proxy origin timeout**.

Apply this sub-checklist when the symptom is a 524/522/504 on a request with `Content-Length > ~500 KB`:

1. **Locate the edge/reverse-proxy access log** (Caddy, nginx, Envoy, Cloudflare Logpush).
2. **Compare `bytes_read` (or equivalent) to `Content-Length`**:
   - `bytes_read == Content-Length` and `status` is an error → likely backend/processing problem.
   - `bytes_read < Content-Length` and the connection closed around the timeout window → **upload problem**.
3. **Check `duration` / `request_time` semantics**:
   - Caddy `duration` = wall time from first byte read to response end.
   - nginx `$request_time` = same.
   - <upstream-capture-service> / app `request_time` = time backend spent processing after body was fully received.
   - If proxy `duration` ≈ timeout but upstream `request_time` is short or never logged, the body upload is the bottleneck.
4. **Look for `status=0` (Caddy) or `-` (nginx)**:
   - `status=0` means the proxy never wrote an HTTP response, usually because the downstream/client side closed first.
5. **Correlate with upstream logs**:
   - If the request ID / ray ID / trace ID **does not appear** in upstream (<new-api-container>, <upstream-capture-service>, app) logs, the request never finished uploading.

**Example signature of an upload-timeout 524:**

```json
{
  "status": 0,
  "duration": 125.0,
  "bytes_read": 4111422,
  "request": {
    "headers": { "Content-Length": ["6042141"] }
  }
}
```

Interpretation: the proxy kept the connection for 125 s, read 4.1 MB of a 6 MB body, then Cloudflare closed it and returned 524.

**Example signature of a processing-timeout:**

```json
{
  "status": 504,
  "duration": 120.1,
  "bytes_read": 6042141,
  "request": { "headers": { "Content-Length": ["6042141"] } }
}
```

Interpretation: full body uploaded, but backend did not respond before proxy timeout → backend/processing problem.

### Step 0.7: Throughput collapse — when nothing errors and everything is slow

The recipe above (0.6) handles a *timeout* — an error code you can grep for. This one handles the family with **no error at all**: every request succeeds, every health check is green, and the system is unusable because bytes arrive at a fraction of the expected rate. It is the failure mode most likely to be misdiagnosed for hours, because the entire error-driven toolkit has nothing to bite on.

**Why the usual signals go green** — each of these was observed simultaneously with a 100×-degraded link:

| Signal | What it read | Why it is green anyway |
| --- | --- | --- |
| HTTP status | `200` on everything | Slow is not an error; the response completes, just late |
| Round-trip latency | 12 ms | RTT measures a small packet's round trip; it is **not** a capacity measurement |
| Liveness/health endpoint | `0.04 s` | Its response is handshake-sized, so its timing is all RTT and none of it transfer. **A probe that finishes before the link becomes the limiting factor cannot measure the link** — see Principle 5 |
| Transport status field | `active; direct <endpoint>` | Reports the **path type**, not the path's capacity — see trap 16 |

So the first move is to stop reading status and **measure a rate**.

For device or wired/Wi-Fi comparisons, load [device throughput fieldwork](references/device-throughput-fieldwork.md) before probing or changing association; bind the control/data paths, comparison and recovery window there.

**The decisive experiment: two independent channels, same direction, same time budget.**

This is Principle 3's layered isolation applied to capacity instead of correctness. Pick two channels that reach the same host over the same network path but share **no application code** — the point is that a result they agree on cannot be caused by either one's internals.

Both commands below are **time-budgeted, not size-budgeted**: they stream for a fixed number of seconds and report whatever arrived. That keeps the probe's cost constant no matter how bad the link is — a fixed 8 MB payload took under a second on the healthy path in the case study and about 90 seconds on the degraded one, and a degradation deeper than that scales the wait without bound, exactly when you can least afford it. It is also why the two numbers are comparable: same seconds, same direction, both measured **at the receiving end**.

```bash
BUDGET=20

# Channel A — the service under suspicion. Point it at the largest object you can name.
# --max-time aborts mid-transfer; -w still prints, and curl exits 28. That is a
# successful measurement, not a failure — read the rate, ignore the exit code here.
curl -s -o /dev/null --max-time "$BUDGET" \
  -w 'A: %{size_download} B in %{time_total}s = %{speed_download} B/s\n' \
  "http://<host>:<port>/<a-large-object>"

# Channel B — a completely different stack to the same host. `cat /dev/zero` streams
# until cut off, so the byte count is decided by the link, not by the payload size.
B=$(timeout "$BUDGET" ssh <host> 'cat /dev/zero' 2>/dev/null | wc -c)
echo "B: $B B in ${BUDGET}s = $((B / BUDGET)) B/s"
```

> **Do not substitute `dd`'s own summary line for channel B's number.** `dd if=/dev/zero … ` reports how fast it wrote into *its stdout*, which for a piped SSH transfer is absorbed by the pipe and SSH's channel window — it measures the local write, not the wire, and it never sees connection setup. Measured on the case study's healthy path, raw SSH moved 33.6 MB end-to-end at 44 MB/s while `dd` self-reported 54.9 MB/s on the same transfer. A sender-side rate is an upper bound on the wire rate, never a measurement of it. Time the whole invocation from the receiving end, as above.

**Expect the two channels to disagree in absolute terms — that is not a problem.** On the same healthy path, raw SSH measured ~44 MB/s while fetches through the media service measured 11–16 MB/s, because channel A pays for the service's own read and encode work on top of the wire. This is why the decision rule below is about **order of magnitude**, not equality: what you are testing is whether both channels sit in the same band, not whether they print the same number. Two channels that differ by 3× on a healthy link and both collapse to 0.1 MB/s on a degraded one have told you exactly what you needed.

**Before reading the table, confirm both probes actually ran.** This is trap 17 applied to your own recipe, and it is the easiest way to get a confidently wrong answer here: a probe that never transferred anything produces a *low or zero rate*, which the table below reads as "slow" and routes straight to "the path" — a conclusion drawn from a measurement that never happened. Two checks, both cheap:

- **Channel A**: is `size_download` close to the object's real size? A DNS failure (curl exit 6), a refused connection (exit 7), or a 404 error page all return quickly and tiny — which can even read as *fast* and land you in the wrong row from the other direction. Only exit 28 (`--max-time` fired mid-transfer) means "the measurement is good, the object was just bigger than the budget".
- **Channel B**: is `$B` plausibly non-zero? `2>/dev/null` in that command hides SSH's own errors, so a host you cannot authenticate to yields `B=0` silently. Re-run it once without `2>/dev/null` if the number looks wrong.

Read the result as a four-way test — all four outcomes are reachable, and three of them tell you to stop what you were about to do:

| Channel A | Channel B | Conclusion |
| --- | --- | --- |
| slow | slow | **The path.** Two unrelated stacks cannot be slow for unrelated reasons at the same rate. Stop tuning the application — nothing inside it will help. Go to "what changed about the path" below. |
| slow | fast | **The application/service.** The path can clearly move bytes; A cannot. You now have a working control to bisect against. |
| fast | slow | **Neither conclusion yet — your control is the anomaly.** SSH-specific cost (cipher on a weak CPU, a hop that shapes SSH), or B crossed a different path. Do not conclude "the service is fine"; replace B with a third stack and re-run. |
| fast | fast | **The probe did not reproduce the symptom.** Do not declare the incident closed. The real workload differs in some dimension you have not replicated — direction (upload vs download), concurrency, object size, or time of day. Change one of those and re-measure before believing the green result. |

**How close is "the same rate"?** Treat agreement within **~20%** as the same rate for this purpose — the two channels differ in framing, encryption and per-object overhead, so exact equality is not expected and not required. The judgement is order-of-magnitude: two stacks reading 0.09 and 0.11 MB/s agree; 0.1 and 12 MB/s do not. If the gap is between ~20% and ~2×, treat it as unresolved and widen the budget or the object size rather than picking a side.

**Pairing channel B when there is no SSH.** The requirement is only *same host, same direction, no shared application code* — SSH is convenient, not special. Any of these works: a second unrelated service on the host (a metrics endpoint, an object store, a static file server), a container-runtime transfer (`docker cp` from that host), a raw throughput tool if you can run one on both ends (`iperf3 -c`), or the host's own package/artifact mirror. What does **not** count is a second endpoint of the same service — that shares the code you are trying to exonerate, so agreement proves nothing.

**Then find what changed about the path.** Once the path is implicated, the variable is usually topology, not hardware: which route the traffic actually takes today versus yesterday. For mesh VPNs (Tailscale, Nebula, ZeroTier) and split-tunnel proxies, the same logical address can be served by a LAN-direct path, a WAN-direct path, or a relay — with order-of-magnitude different capacity and **no change in the status field or the address you connect to**. Ask the transport to report the path it is actually using (`tailscale ping <host>` prints the endpoint and whether the reply came via a relay).

If no prior known-good measurement exists to compare against — the common case in a first incident — you can still make progress without one, because the question "did this change?" has a cheaper substitute: **measure the same path from somewhere else.** A second client on a different network, or the host measuring *itself* over loopback, brackets the problem without any history. Then record today's number as the baseline you did not have; the second occurrence of this incident is much cheaper than the first, and only if someone writes the number down.

**Finally, audit what the wrong assumption already broke.** A degraded link does not just make things slow — it silently invalidates every timeout you calibrated on the fast path, and those timeouts produce *corrupt artifacts that look like successes*. Before declaring the incident over, re-verify anything transferred during the degraded window; see trap 18 and Step 7.

**Replay the original workload before changing its semantics.** After a path or
node change restores a representative transfer rate, rerun the exact operation
that originally failed with its original completeness contract: a full Git clone
for a clone incident, the full body for an upload incident, or the original
streaming protocol for a stream incident. A small health request, `ls-remote`, or
partial/shallow clone proves only that its own smaller path works. If the original
workload now completes inside its existing deadline, the evidence refutes the need
for an application-level workaround such as partial clone or a relaxed timeout.
If it still fails, keep that result and only then investigate workload semantics.
This follows RFC 6349's separation of sustained TCP throughput from RTT/liveness:
the proof must exercise enough data for transfer capacity to dominate setup cost.

### Step 1: Gather direct evidence at every hop

Before calling an unfamiliar device or local-client HTTP/RPC endpoint for evidence, load [device throughput fieldwork](references/device-throughput-fieldwork.md#classify-an-unfamiliar-endpoint-before-calling-it); finish offline discovery and classify effects before a separate execution call.

Before framing hypotheses, collect:

- Server-side logs at every hop in the request path
- Client-side logs (browser devtools HAR, CLI debug log, SDK traces)
- Metrics over the incident window (RPS, latency, error rate, connection count, CPU/mem)
- Distributed trace if available
- Packet capture if the symptom is at the wire level (see [references/packet-capture-recipes.md](references/packet-capture-recipes.md))

If any of these is missing and relevant, **fill the gap before guessing**. Adding a `TRACE_*` env flag and restarting a container beats an hour of hypothesis-stacking. The instrumentation patterns in [references/instrumentation-patterns.md](references/instrumentation-patterns.md) are low-risk, env-gated, and safe to ship into production permanently.

#### Reading reverse-proxy access logs for upload/processing split

Caddy and nginx logs are the cheapest way to falsify "backend is slow". Focus on three fields:

| Field               | Caddy JSON key                   | nginx var                 | Meaning                                                            |
| ------------------- | -------------------------------- | ------------------------- | ------------------------------------------------------------------ |
| Total wall time     | `duration`                       | `$request_time`           | First byte from client → last byte to client (or connection close) |
| Body bytes received | `bytes_read`                     | `$request_length` (rough) | Bytes the proxy actually read from the client...

© daymade · MIT · daymade/claude-code-skills

Similar skills

Everything for DevOps and infrastructure →

From the same set64

douban-skill Pulls books, movies, music and games out of a Douban profile into CSV files, with RSS sync for new entries. CX Codex +1 749installs terraform-skill Works out why terraform plan says "No changes" while the site is down, and keeps a rollout from touching someone else's service on a shared host. CX Codex +1 710installs marketplace-dev Turns a skills repo into a Claude plugin marketplace: generates marketplace.json, adds plugin entries, and moves skills between suites. CX Codex +1 695installs tunnel-doctor Diagnoses network routing and throughput failures caused by Tailscale, proxy, and TUN conflicts on macOS or Windows/WSL. Helps when pings succeed but SSH or HTTP fails. CX Codex +1 678installs competitors-analysis Clones competitor repositories and audits their code into factual summaries with exact file and line citations. Use it to compare architecture decisions using actual commits. CX Codex +1 659installs slides-creator Retirement notice entry: explains the deprecation status of the tool and no longer generates presentation decks. CX Codex +1 652installs stepfun-tts Generates Chinese and Japanese speech with StepFun Contextual TTS. Controls emotion and prosody using natural-language instructions and inline brackets. CX Codex +1 639installs frontend-visual-qa Checks the interface you can actually see in a real browser: typography, overflow, responsive layout, routes, charts. Useful right after UI work, when the build passes but nobody looked. CX Codex +1 572installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor