Skip to content

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

Description

@ericwu917

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

  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.

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