[BUG] Established connections to the API go silent; client waits 180s+ before retrying (retry then succeeds in ~1.5s)
Summary
Roughly 5.5% of /v1/messages requests never receive a response on the connection they were dispatched on. The connection is not reset and raises no socket error — it simply stops delivering. The client waits out its full byte-idle deadline (180s by default), aborts, and retries. The retry then succeeds in ~1.5 seconds, every time.
The failure is not the network being down: other HTTP requests to api.anthropic.com from the same process succeed during the stall. It is a single established connection being silently blackholed while its siblings work.
The user-visible effect is a session that appears frozen for 3+ minutes with no output, which most people kill manually before the retry ever fires.
Environment
- Claude Code 2.1.258 (also reproduced on 2.1.252), native install
- macOS 26.5.1, arm64
- Direct Anthropic API (
firstParty), no proxy, no VPN, no gateway
- Model:
claude-opus-4-8
- No custom timeout env vars for the baseline measurements below
Two signatures, one cause
A. No response headers ever arrive
11:44:15.387 [ERROR] [first-byte] no response headers 196s after dispatch — aborting request
11:44:15.387 [ERROR] API error (attempt 1/11): undefined Connection error.
11:44:15.923 [DEBUG] [API:timing] dispatching to firstParty model=claude-opus-4-8
11:44:17.211 [DEBUG] Stream started - received first chunk
11:44:17.211 [DEBUG] [API:timing] first byte after 1290ms
The request is dispatched and no HTTP response begins at all — not a slow first token, no status line. The immediate retry connects in 1290ms.
B. Stream dies mid-delivery
11:54:55.760 [WARN] [Stall] stream_idle_partial lastChunkAgeMs=15000 bytesTotal=804 idleDeadlineMs=180000
11:55:10.760 [WARN] [Stall] stream_idle_partial lastChunkAgeMs=30000 bytesTotal=804 idleDeadlineMs=180000
11:55:40.759 [WARN] [Stall] stream_idle_partial lastChunkAgeMs=60000 bytesTotal=804 idleDeadlineMs=180000
11:56:40.761 [WARN] [Stall] stream_idle_partial lastChunkAgeMs=120001 bytesTotal=804 idleDeadlineMs=180000
11:57:40 [WARN] [byte-watchdog] firing: idle=180000ms late=1ms errored=false bodyReadPending=true
bytesTotal is frozen at 804 across all four checks. Note this is not the keepalive-vs-idle-threshold interaction described in #69238: there, pings arrive every 30s and bytesTotal grows ~36 bytes each time. Here zero bytes arrive, including no keepalives.
errored=false, late=1ms, bodyReadPending=true — the socket reports no error and the process was not suspended. It is simply waiting on a body that never comes.
The network is demonstrably up during the stall
During the 196s stall in signature A, the same process completed six CCRClient heartbeats and presence pulses to api.anthropic.com:
11:42:23 CCRClient: Heartbeat sent
11:42:44 CCRClient: Heartbeat sent
11:43:05 CCRClient: Heartbeat sent
11:43:24 CCRClient: Heartbeat sent
11:43:45 CCRClient: Heartbeat sent
11:44:04 CCRClient: Heartbeat sent
11:44:15 [ERROR] [first-byte] no response headers 196s after dispatch
Same host, same machine, overlapping in time. Only the /v1/messages connection was dead.
Measurements
1.7 hours, 6 concurrent sessions, from --debug-file … -d api:
| metric |
value |
/v1/messages requests |
275 |
| failures |
15 (5.5%) |
| — no-headers |
12 |
| — mid-stream |
3 |
| successful first-byte |
median 1358ms, p90 2197ms, max 10738ms |
| recovery after failure (n=12) |
1290–2751ms, 100% on the first retry |
Per-session rates ranged 0%–12% across 23–90 requests each. Failures did not correlate with request body size (0.57 MB → 8.0%, 0.95 MB → 12.0%, 2.05 MB → 8.7%), nor with session age, nor with idle time before dispatch (failures occurred 0s, 16s and 42s after the previous successful byte, and one died mid-stream while actively receiving).
Failures arrive in bursts across independent processes simultaneously — four different sessions failed within five minutes of each other — which rules out anything session-local (MCP servers, hooks, permission mode, context size).
Why this is worse than it needs to be
The failure itself may be outside the client's control, but the client's response amplifies it:
- 180s is a long time to wait for a connection that will never answer, when reconnecting demonstrably takes ~1.5s and succeeds 100% of the time. Slow first bytes top out at 10.7s in this sample — there is a wide, clean gap between "slow but alive" and "dead".
- The obvious tuning knob is inverted.
CLAUDE_STREAM_IDLE_TIMEOUT_MS runs through Math.max(env, 300000), so setting it below the 180s default silently yields 300s — worse than doing nothing. Only the undocumented CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS can lower it. This is very easy to get backwards.
- The user-facing message points at the wrong thing.
check your network is shown while the client's own heartbeats to that same host are succeeding.
Suggested improvements
- Treat "no response headers after N seconds" as a distinct, shorter deadline than "stream went idle mid-delivery". A connection that has produced zero bytes is far more likely dead than slow.
- Consider a much lower default first-byte deadline (30–60s) given measured recovery is ~1.5s at 100% success.
- Document
CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS, or remove the 300s floor on CLAUDE_STREAM_IDLE_TIMEOUT_MS so the documented variable can actually lower the deadline.
- Drop
check your network when recent heartbeats to the same host have succeeded, and name the failing component instead.
Workaround
{
"env": {
"CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS": "45000",
"CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS": "45000"
}
}
Failures still occur at the same rate, but cost ~45–75s instead of 180–198s before automatic recovery. Note CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS is read from the settings object and applies without restart; CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS is read from process.env and requires one.
Related
Possibly related to #69238, but distinct: that issue describes keepalive pings arriving and being miscounted as idle. Here no bytes arrive at all, and it reproduces without the advisor tool (disabled via CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1).
[BUG] Established connections to the API go silent; client waits 180s+ before retrying (retry then succeeds in ~1.5s)
Summary
Roughly 5.5% of
/v1/messagesrequests never receive a response on the connection they were dispatched on. The connection is not reset and raises no socket error — it simply stops delivering. The client waits out its full byte-idle deadline (180s by default), aborts, and retries. The retry then succeeds in ~1.5 seconds, every time.The failure is not the network being down: other HTTP requests to
api.anthropic.comfrom the same process succeed during the stall. It is a single established connection being silently blackholed while its siblings work.The user-visible effect is a session that appears frozen for 3+ minutes with no output, which most people kill manually before the retry ever fires.
Environment
firstParty), no proxy, no VPN, no gatewayclaude-opus-4-8Two signatures, one cause
A. No response headers ever arrive
The request is dispatched and no HTTP response begins at all — not a slow first token, no status line. The immediate retry connects in 1290ms.
B. Stream dies mid-delivery
bytesTotalis frozen at 804 across all four checks. Note this is not the keepalive-vs-idle-threshold interaction described in #69238: there, pings arrive every 30s andbytesTotalgrows ~36 bytes each time. Here zero bytes arrive, including no keepalives.errored=false,late=1ms,bodyReadPending=true— the socket reports no error and the process was not suspended. It is simply waiting on a body that never comes.The network is demonstrably up during the stall
During the 196s stall in signature A, the same process completed six
CCRClientheartbeats and presence pulses toapi.anthropic.com:Same host, same machine, overlapping in time. Only the
/v1/messagesconnection was dead.Measurements
1.7 hours, 6 concurrent sessions, from
--debug-file … -d api:/v1/messagesrequestsPer-session rates ranged 0%–12% across 23–90 requests each. Failures did not correlate with request body size (0.57 MB → 8.0%, 0.95 MB → 12.0%, 2.05 MB → 8.7%), nor with session age, nor with idle time before dispatch (failures occurred 0s, 16s and 42s after the previous successful byte, and one died mid-stream while actively receiving).
Failures arrive in bursts across independent processes simultaneously — four different sessions failed within five minutes of each other — which rules out anything session-local (MCP servers, hooks, permission mode, context size).
Why this is worse than it needs to be
The failure itself may be outside the client's control, but the client's response amplifies it:
CLAUDE_STREAM_IDLE_TIMEOUT_MSruns throughMath.max(env, 300000), so setting it below the 180s default silently yields 300s — worse than doing nothing. Only the undocumentedCLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MScan lower it. This is very easy to get backwards.check your networkis shown while the client's own heartbeats to that same host are succeeding.Suggested improvements
CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS, or remove the 300s floor onCLAUDE_STREAM_IDLE_TIMEOUT_MSso the documented variable can actually lower the deadline.check your networkwhen recent heartbeats to the same host have succeeded, and name the failing component instead.Workaround
{ "env": { "CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS": "45000", "CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS": "45000" } }Failures still occur at the same rate, but cost ~45–75s instead of 180–198s before automatic recovery. Note
CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MSis read from the settings object and applies without restart;CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MSis read fromprocess.envand requires one.Related
Possibly related to #69238, but distinct: that issue describes keepalive pings arriving and being miscounted as idle. Here no bytes arrive at all, and it reproduces without the advisor tool (disabled via
CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1).