[BUG] Connection pool reuses sockets past the 400s Cloudflare idle timeout, causing ECONNRESET
Summary
Claude Code keeps HTTP keep-alive sockets in its pool beyond the api.anthropic.com edge
idle timeout of 400 seconds, then writes requests onto sockets the server has already
closed. The result is a steady stream of ECONNRESET errors whose frequency scales with
how long a session has been idle.
Reproduced on two separate machines, both macOS, both consumer networks, no VPN or proxy.
Environment
- Claude Code 2.1.218 (native install), desktop bundle 2.1.237
- macOS 15 (Darwin 25.5.0), Node v24.19.0
- No proxy, no VPN, no security extensions. Wi-Fi 6E at -47 dBm, zero interface errors
- Gateway: TP-Link Deco mesh, single NAT, no CGNAT
Measurements
Across 511 local transcripts: 31,315 assistant turns, 2,109 ECONNRESET events (6.7%).
Reset rate by session idle time before the request:
| Idle before request | Reset rate |
| --- | --- |
| under 10s | 4.2% |
| 10 to 60s | 13.3% |
| 1 to 5 min | 16.3% |
| 5 to 15 min | 42.1% |
The server-side timeout is exactly 400 seconds. Probe connections held idle, half withSO_KEEPALIVE enabled:
300s idle, keepalive off -> ALIVE, reuse OK
300s idle, keepalive ON -> ALIVE, reuse OK
600s idle, keepalive off -> FIN at 400.0s
600s idle, keepalive ON -> FIN at 400.0s
1800s idle, either -> FIN at 400.0s
Staggering connection births by 60s produced deaths staggered by 60s, each at age 400.0s.
That confirms a per-connection idle timer rather than a single edge event.
Host comparison, run simultaneously from one machine:
| Host | Idle timeout |
| --- | --- |
| github.com | 30s |
| www.google.com | 240s |
| cloudflare.com | 400.0s |
| api.anthropic.com | 400.0s |
Per-host values rule out a local middlebox, which would impose one timeout on every flow.cloudflare.com matching api.anthropic.com identifies 400s as the Cloudflare edge
keep-alive timeout.
Why this points at the client
Closing idle keep-alive connections is correct server behavior. The defect is that the pool
holds sockets past that boundary and reuses them without revalidation. During this
investigation the client held 90 established connections to 160.79.104.10, so a large
fraction of the pool can be older than 400s at any moment.
Retry behavior fits a pool of dead sockets rather than a transient network fault. The
conditional probability that a retry also fails rises with attempt count, from 0.56 at
attempt 2 to 0.83 by attempt 10, consistent with successive retries drawing further stale
sockets from the same pool. Of roughly 710 affected requests, 49 exhausted all 10 retries.
Suggested fix
Set the agent's free-socket idle timeout below the edge timeout so the client discards
sockets before the server does. 240s would leave headroom under 400s. Retrying idempotent
requests once on a reset from a reused socket is the standard remedy for this race.
Workaround in use
CLAUDE_CODE_MAX_RETRIES=20 via settings.json, which masks the symptom and adds about
6 minutes before a doomed request surfaces. No client setting exists for pool or keepalive
tuning, so the cause is not addressable from the user side.
Reproduction
Leave a session idle for 7 to 10 minutes, then send a request. The first request after the
gap resets far more often than one sent during continuous activity.
Related issues
Likely the same root cause: #87424, #81847, #75956, #86632, #87599.
On #87599 specifically: that report rules out an idle timeout on the grounds that resets
arrive in simultaneous bursts of 10 to 15 connections within ~3ms, arguing a per-flow
problem would kill one connection at a time. A connection pool opens its sockets within
milliseconds of each other during parallel work, so a per-connection 400s timer expires
all of them within milliseconds of each other 400 seconds later. The burst pattern is what
a per-connection idle timeout looks like on a pooled connection set, so that evidence
supports the idle-timeout explanation rather than excluding it.
The staggered-birth measurement above was run specifically to separate those two cases.
3 Comments
Adding a second platform + a data point that helps rule out the concurrency angle. cli disconnects continuously so feedback file can't be sent as normal.
Environment
Initially looked concurrency/auto-mode-specific, but isn't
The failures first appeared correlated with
permissions.defaultMode: auto— the safety classifier failed 3/3 times when 4+ tool calls were batched into one turn. But:Invoke-WebRequest) fired in the same batch toapi.anthropic.com,claude.ai,console.anthropic.com, and a GCP-hosted endpoint succeeded 100% (30+ requests, zero failures) — so this isn't TLS/cert/network-path flakiness at the transport level.permissions.defaultModefromautotodefaultdid not stop the disconnects — same error class kept occurring. That rules out the classifier/concurrency path as root cause and points at the general HTTP client instead, consistent with this issue.Debug log, zero concurrent tool calls
Captured from
~/.claude/debug/<session>.txton a session with a single trivial "reply pong" turn — no batching, no concurrency — failure still occurs within 5 seconds of startup:17:33:12.073Z [ERROR] API error (attempt 1/11): undefined Connection error.
17:33:12.631Z [DEBUG] Stale connection — disabling keep-alive for retry
17:33:15.059Z [ERROR] 1P event logging: 129 events failed to export (code=ECONNRESET, socket hang up)
17:33:15.060Z [ERROR] [3P telemetry] OTEL diag error: {"message":"Failed to export 129 events (code=ECONNRESET, socket hang up)"...}
The client's own log line —
Stale connection — disabling keep-alive for retry— names the mechanism directly: a pooled keep-alive socket getting reused after the server side already closed it, matching the 400s-edge-idle-timeout theory in the original report.attempt 1/11is the defaultCLAUDE_CODE_MAX_RETRIES=10(no override set), so this isn't a config difference.The telemetry detail may also be useful: 129 OTEL events failed to export in one shot, not as scattered per-event failures. That reads as one bad reused socket taking down a whole batched write, rather than independent per-request flakiness — consistent with a single stale connection object being reused across both API and telemetry traffic rather than N separate transient failures.
Net: same root cause as the original report (stale pooled connection reused past the server's idle timeout), reproduced on Windows/PowerShell with no proxy/VPN in the picture and with auto-mode/concurrency explicitly ruled out as contributing factors.
Adding the other half of this: the same 400s edge timeout also kills sockets INSIDE a single turn, not only across idle gaps between turns. That produces a hard, narrow wall instead of a probabilistic reset rate, and it may be an easier repro to point an engineer at.
Setup
Five long-running Claude Code sessions in tmux, each driven by an inbound Telegram message, so turns routinely run several minutes of tool work with no request on the wire. macOS 15 (Darwin 25.3.0), Apple Silicon, Max plan, claude-opus-5. Failures seen on 2.1.221, 2.1.245, 2.1.251 and 2.1.252.
The wall
Any turn longer than about 345 s of wall clock ends with "API Error: Connection dropped (ECONNRESET)" after exhausting the retry counter. 36 dead turns measured on 01 Sep 2026. Time from the last transcript entry before the failing step to the API Error entry:
min 335 s, median 345 s, max 360 s. Nothing outside that band.
Longest in-turn step that COMPLETED, by day:
23 Aug: 1275 s (21 minutes, finished normally)
30 Aug: 303 s
31 Aug: 291 s
01 Sep: 330 s
Nothing above 330 s has completed since 30 Aug. Nothing in the 335 to 360 s band has ever survived.
Onset is sharp
Counting every assistant entry whose text starts with "API Error", across all five sessions' transcripts:
29 Jul 2026: 2
30 Jul to 29 Aug 2026: 0 (a full month, roughly 10,000 in-turn steps)
30 Aug 2026: 10
31 Aug 2026: 15
01 Sep 2026: 33 (partial day)
A 1275 s turn completing on 23 Aug and a 345 s turn dying on 30 Aug is the whole regression in two numbers.
Probes, agreeing with yours
www.cloudflare.com, TLS flow held silent 400 s, then reused: ALIVE, answered normally.
api.anthropic.com, same test: alive at 120 s, CLEANLY CLOSED at 400 s. FIN, never RST.
The cloudflare.com result rules out NAT, router and ISP by measurement rather than by argument, which matches your per-host table.
Why the band is narrow here and probabilistic in your data
Your table measures idle before the request, that is, gaps between turns, where socket age at reuse varies freely. In a long tool-executing turn the socket is created at the turn's first request and then sits idle for the whole tool run, so its age at the next write IS the turn duration. The edge FINs at 400 s, the client writes into the half-closed socket, and the user sees ECONNRESET. That predicts a ceiling near but below 400 s, which is exactly the 335 to 360 s band observed.
Ruled out with controls
CLI version: identical failure on four releases.
Transcript size: fails on a 55 MB transcript and on 0.8 MB ones in the same hour.
Effort level: CLAUDE_CODE_EFFORT_LEVEL=max was in place from 25 Aug while breaks began on the 30th. Dropping to high shortens turns but does not move the ceiling.
Related: #90964 reports the same thing from the user side (drops after 6m02s, 5m44s and 5m47s of active work, macOS, 2.1.251) with no measurements attached.
Update with more data. The idle-socket finding holds for one-off resets, which retries recover.
The fatal "Connection dropped" loops are a separate problem.
bad record mac). The same uploads to AWS succeed.Details and asks are in my comment on #74758.
Two corrections to the original report:
CLAUDE_CODE_MAX_RETRIES=20was silently clamped to 15 (now documented).