[Bug] Streams die under long thinking gaps: 180s first-party idle abort + retry permanently disabled after one visible block
Preflight Checklist
- [x] I have searched existing issues — many open reports carry this symptom (#70217, #81848, #82994, #84551, #80408, #83943 among others); none identifies the client-side mechanism below. #84404 is related but distinct: there the retry loop runs all 10 attempts against a custom endpoint; here the retry is disabled by a gate on the first-party path.
- [x] This is a single bug report — two defects, but they share one code path and one measurement corpus, and separating them would orphan the evidence.
- [x] I am using the latest version of Claude Code
What's Wrong?
API Error: Connection closed mid-response / Response stalled mid-stream on nearly every long free-form turn. Measured over 8 days of real transcripts: 246 truncated streams in 67,235 API calls, counting only records with isApiErrorMessage: true (the transcript's error field reads "server_error" even for client-side aborts, so it cannot be used to classify — the message text can).
Environment: Claude Code 2.1.226, native first-party endpoint (no proxy — verified 0 connections on the only local gateway), macOS 25.5, Opus, high effort, median context 220–280k.
Two separable client-side defects:
Defect 1 — first-party path aborts healthy requests at 180 s, below the documented 300 s floor
$SS() takes NSS = 180000 when the provider is "firstParty", while the env-var path floors at 300 s:
function Cba(){ return Math.max(PK(process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS)||0, 300000) }
function $SS(e){ let t = Cba(), r = e === "firstParty" ? NSS : t, ... } // NSS = 180000
With high effort and a large context the model routinely stays silent longer than 3 minutes before the first byte; the client kills a request that was fine.
Evidence it is a hard timer, not a distribution: request age at abort has p10 = 183 s and nothing below it — a step function at the predicted 180 s (measured median 192 s).
Raising it requires two variables, because the smaller one wins:
Vn = te.CLAUDE_ENABLE_STREAM_WATCHDOG ?? !0 // default ON
En = Cba() // 300000 when unset
di = Math.min( Aba(Kn()), Vn ? En : 1/0 ) // BYTE_ override alone loses to 300000
CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS alone is clamped to 300 s; only setting CLAUDE_STREAM_IDLE_TIMEOUT_MS as well lifts the deadline. Measured effect with both at 900000: watchdog aborts went 70 (preceding 9 days) → 0, while connection-resets were unaffected — which separates defect 1 from defect 2 cleanly.
Ask: apply the 300 s floor to the first-party path too, or document NSS and surface the effective deadline in the error text. Note also that the retry banner renders (deadline − 20000), so raising the deadline inflates the user-facing "will retry in Xm" wait — at 900000 it shows "14m".
Defect 2 — one yielded visible block permanently disables retry, so identical faults get opposite outcomes
The retry gate is if (!bt && ...), where bt latches true as soon as any content block that is not thinking/redacted_thinking is yielded. Same session, same error code, ~10 minutes apart:
23:59:33 [WARN] Stream connection error (ECONNRESET) — retrying streaming (1/10)
00:10:05 [WARN] Stream connection closed (ECONNRESET) after 1 block(s) yielded
— finalizing partial response
The first recovered invisibly. The second surfaced as API Error: Connection closed mid-response. One yielded block was the entire difference; the retry budget of 10 was never used past 1.
This is why the failure looks user-specific. Everyone's streams get reset; a reset during thinking is retried invisibly (measured: 71% of resets never reach the screen). A workload that emits visible output early and then keeps streaming for minutes spends almost all of its exposure in the non-retryable state. Measured by turn shape: tool_use turns truncate at 0.2 per 1k, free-form text turns at 15.3 per 1k — 75×, with no corresponding difference in the underlying fault.
Ask: resume after partial output where the API allows it, or at minimum report blocks_yielded and the remaining retry budget in the user-facing error so the difference is not invisible.
Contributing condition — minutes with zero bytes in either direction
During a long thinking gap the connection carries nothing either way. TCP-level keep-alive (tested: always_keepalive=1, keepidle=30s, persisted via LaunchDaemon) did not reduce resets. An application-level keep-alive — an SSE comment line, or an HTTP/2 PING — would keep the flow provably alive through middleboxes where TCP-level demonstrably does not. Combined with defect 2, a single reaped flow costs the whole turn's output.
Ruled out locally, with the measurement that ruled it out
TCP keepalive/NAT idle (applied, no change) · PMTU (v4 1480 / v6 1500 OK) · local proxy (0 connections) · process reapers (none touch claude) · memory pressure (wrong hours) · account rate limit (1.2% of block consumed at reset moment) · link quality at the reset minute (6/6 TLS handshakes, TTFB 40–136 ms) · turn duration · context size (weak: 0.9→3.9/1k vs 75× for turn shape) · CLI version (non-monotonic once double-counting is removed) · session concurrency (flat per call, 1→6 sessions). A local link-flap investigation continues on my side — but defects 1 and 2 are client-code behavior, reproducible regardless of that outcome.
Request ids for server-side correlation
Night burst (2026-08-09, one session, 8 resets in ~64 min, UTC):
req_011CdrCsyJNiCnR8xPb6vZGW 00:12:04Z
req_011CdrFKuoF7UPTdNmLyV5VV 00:43:37Z
req_011CdrFYthsMrhr9iMwNKsrp 00:47:07Z
req_011CdrFnojcRbGu1Jjv4n2vd 00:49:38Z
req_011CdrG4kXaYLsbqPJjBZfW6 00:53:38Z
req_011CdrG9BUdB9XNJm5k1Sf8U 00:55:08Z
req_011CdrGDZFfqYeAYSBLyBXTn 00:55:39Z
req_011CdrHoe4hnVHVpWV1scvDv 01:16:10Z
Morning burst (2026-08-09, three concurrent sessions, UTC):
req_011Cds9P5fD6omqsKwG2u3Am 12:07:39Z
req_011Cds9eJwqPde1KXU9wgafq 12:10:04Z
req_011CdsBv33njkwAFBbmcAfxS 12:39:24Z
req_011CdsDcjzsrXsgXP2yJirmS 13:03:26Z
~160 further paired request ids over the 8-day corpus available on request (pairing: nearest preceding requestId record in the session transcript; validated 32/32 over the last two days).
Environment Info
- Platform: darwin 25.5.0
- Version: 2.1.226 (native install,
latestchannel) - Endpoint: first-party Anthropic (no
ANTHROPIC_BASE_URLin any scope)
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗