[BUG] Stalls before response headers have only the 600s API_TIMEOUT_MS backstop — silent 3-10 minute hangs with no error or retry
Preflight Checklist
- [x] I have searched existing issues — #70008 (60 s "Request timed out" regression) and #82865 (connection closed mid-response) describe different phases; this one is specifically about stalls before response headers arrive, where no short deadline exists at all.
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
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
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_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_MSactionable 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.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗