anthropics / anthropics/claude-code

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

Open
#91,502 0 comments 0 reactions 0 assignees View on GitHub
area:api area:networking bug has repro platform:macos
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

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

```json
{
"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`).

Contributor guide

No contributing guide indexed for this repository

Research direction

The issue names no source files or tests. Start at /v1/messages timeout handling, tracing CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS from the settings object and CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS from process.env. Reproduce the supplied debug timing patterns and verify that silent connections abort and retry promptly, mid-stream stalls remain distinct, and the resulting message identifies the relevant failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.