Skip to content

[BUG] Established connections to the API go silent; client waits 180s+ before retrying (retry then succeeds in ~1.5s) #91502

Description

@Thegreatbear08

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions