anthropics / anthropics/claude-code

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

オープン
#91,502 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
area:api area:networking bug has repro platform:macos
主要言語
Python
スター
145k
フォーク
23.1k
PR マージ指標
PR 指標を取得中

説明

# [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`).

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

評価

技術スタック
python
領域
api, networking
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
45/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。