[BUG] Stalls before response headers have only the 600s API_TIMEOUT_MS backstop — silent 3-10 minute hangs with no error or retry

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 2, 2026

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 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

  1. 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.
  2. 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.
  3. 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗