Preflight Checklist
Environment
- Claude Code 2.1.220, native install, macOS 15.7.4 arm64
- First-party API (no
ANTHROPIC_BASE_URL); egress via an on-host transparent TUN proxy
What's Wrong?
When a request stalls before response headers arrive (dead pooled connection, black-holed socket), nothing in the client reacts for 10 minutes:
- the byte watchdog wraps
response.body, so it is not armed yet at this point;
- the SDK's timeout timer is cleared as soon as the
fetch promise resolves, so it only covers connect + headers — with the default API_TIMEOUT_MS that is 600 000 ms;
CLAUDE_SLOW_FIRST_BYTE_MS (default 30 000) only writes a debug log line and emits telemetry — it does not abort, retry, or tell the user anything.
The result is a completely silent spinner. Users hit Esc long before the backstop fires, so these failures never even show up as api_error records in the transcript.
Measurements (one workstation, two days)
Stall duration measured from the last tool_result record to the [Request interrupted by user] record:
| Day |
Stall |
Context |
Retry after interrupt |
| 1 |
9 min 25 s |
712k tok |
completed in 12 s |
| 1 |
7 min 19 s |
427k tok |
12 s |
| 1 |
2 min 11 s |
695k tok |
16 s |
| 1 |
5 min 05 s |
858k tok |
27 s |
| 2 |
10 min 11 s (not interrupted — self-recovered) |
— |
retry finished in 11 s |
That last case is the clean one: 611 s = 600 s timeout + 11 s retry. The backstop works exactly as designed; it is simply an order of magnitude longer than the useful recovery time.
Rate on that workstation: ~1866 API requests in a day, 4 hard stalls plus 3 recoveries in the 145–183 s range — roughly 1 stall per 470 requests.
Root cause of the individual stalls (client-side observation)
Caught live: the process held a socket in ESTABLISHED state to the API, while the on-host proxy's connection table no longer had that connection at all — the far side had gone away without FIN/RST. Anything written to it disappeared. Meanwhile a fresh connection from another session on the same host was completing requests in 12–27 s, so the network path itself was healthy. The proxy's exit node showed 100 % probe success (~110 ms) throughout every stall window, and the machine never slept.
What Should Happen?
A request that has produced zero bytes for tens of seconds should be retried on a fresh connection, or at minimum the UI should say something. Today the user cannot distinguish "the model is thinking" from "this connection is dead".
Suggestions
- Give the connect + headers phase its own, much shorter deadline (or make
CLAUDE_SLOW_FIRST_BYTE_MS actionable rather than log-only). Empirically the retry succeeds in 12–27 s, so waiting 600 s buys nothing.
- Retire a pooled connection when a request on it produces zero bytes for N seconds. The code already has a "stale connection → disable keep-alive for retry" path, but it only triggers on errors that never arrive in this scenario.
- Surface something in the UI once a request has been in flight with no bytes for >30 s — the telemetry event for this already exists.
Workaround
~/.claude/settings.json:
"env": {
"API_TIMEOUT_MS": "90000",
"CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS": "60000"
}
This turns a silent 10-minute hang into a ~90 s error plus automatic retry. API_TIMEOUT_MS does not truncate long streaming responses, because the SDK clears the timer once headers arrive.
Preflight Checklist
Environment
ANTHROPIC_BASE_URL); egress via an on-host transparent TUN proxyWhat's Wrong?
When a request stalls before response headers arrive (dead pooled connection, black-holed socket), nothing in the client reacts for 10 minutes:
response.body, so it is not armed yet at this point;fetchpromise resolves, so it only covers connect + headers — with the defaultAPI_TIMEOUT_MSthat is 600 000 ms;CLAUDE_SLOW_FIRST_BYTE_MS(default 30 000) only writes a debug log line and emits telemetry — it does not abort, retry, or tell the user anything.The result is a completely silent spinner. Users hit Esc long before the backstop fires, so these failures never even show up as
api_errorrecords in the transcript.Measurements (one workstation, two days)
Stall duration measured from the last
tool_resultrecord to the[Request interrupted by user]record:That last case is the clean one: 611 s = 600 s timeout + 11 s retry. The backstop works exactly as designed; it is simply an order of magnitude longer than the useful recovery time.
Rate on that workstation: ~1866 API requests in a day, 4 hard stalls plus 3 recoveries in the 145–183 s range — roughly 1 stall per 470 requests.
Root cause of the individual stalls (client-side observation)
Caught live: the process held a socket in
ESTABLISHEDstate to the API, while the on-host proxy's connection table no longer had that connection at all — the far side had gone away without FIN/RST. Anything written to it disappeared. Meanwhile a fresh connection from another session on the same host was completing requests in 12–27 s, so the network path itself was healthy. The proxy's exit node showed 100 % probe success (~110 ms) throughout every stall window, and the machine never slept.What Should Happen?
A request that has produced zero bytes for tens of seconds should be retried on a fresh connection, or at minimum the UI should say something. Today the user cannot distinguish "the model is thinking" from "this connection is dead".
Suggestions
CLAUDE_SLOW_FIRST_BYTE_MSactionable rather than log-only). Empirically the retry succeeds in 12–27 s, so waiting 600 s buys nothing.Workaround
~/.claude/settings.json:This turns a silent 10-minute hang into a ~90 s error plus automatic retry.
API_TIMEOUT_MSdoes not truncate long streaming responses, because the SDK clears the timer once headers arrive.