[BUG] Connection pool reuses sockets past the 400s Cloudflare idle timeout, causing ECONNRESET

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 3 comments · opened Aug 25, 2026

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 with
SO_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.

View original on GitHub ↗

3 Comments

aquariusbrandy-af · 1 month ago

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

  • Claude Code 2.1.246
  • Microsoft Windows 11 Pro 10.0.26200 26200
  • Network: No VPN/corporate proxy

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:

  • Raw HTTPS probes (curl / Invoke-WebRequest) fired in the same batch to api.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.
  • Switching permissions.defaultMode from auto to default did 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>.txt on 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/11 is the default CLAUDE_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.

sorryhumans · 1 month ago

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.

timothysbolger-cyber · 1 month ago

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.

  • They hit only large requests: 3 to 8.5 MB, in image-heavy sessions.
  • They fail on fresh sockets, after the 2.1.214 keep-alive fallback has already fired.
  • On my network, uploads of 1 MB and up to Cloudflare arrive damaged (TLS 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=20 was silently clamped to 15 (now documented).
  • I called my network clean. For large uploads to Cloudflare, it isn't.