Intermittent ECONNRESET / Connection error on /v1/messages — auth OK, transport-level reset, 3 macOS machines

Status Open
Reported on v2.1.199
Maintainer reply None cached
Activity 3 comments · opened Jul 3, 2026

Summary

Persistent intermittent ECONNRESET / "Connection error" on /v1/messages across 3 macOS machines (2× headless Mac mini + 1× MacBook Air), all logged into the same account. A 4th machine (Windows PC) on the same network/WiFi does not exhibit it. The errors are transport-level (auth succeeds on every attempt), so this appears to be a server/edge connection issue rather than a client auth or local network problem.

Symptom

On each failing request:

  1. [API:auth] OAuth token check startingcompletesucceeds every time (not auth-related)
  2. [API REQUEST] /v1/messages ... dispatched
  3. ~40ms later: [ERROR] API error (attempt N/16): undefined Connection error.
  4. Final: code=ECONNRESET, message=The socket connection was closed unexpectedly

Retries eventually recover, but bursts can exhaust the retry budget and abort the turn.

What we ruled out (with evidence)

  • Not auth / token: OAuth token check completes successfully on every single attempt across all logs.
  • Not local network: single curl (both IPv4 and IPv6) to api.anthropic.com from an affected machine succeeds 20/20, fast TLS handshake.
  • Not egress path: routing one affected machine's traffic through a separate SSH SOCKS5 tunnel (different egress IP) did not stop the resets.
  • Not multi-device credential race: each machine has its own independent local .credentials.json (separate files, separate disks, no shared/networked storage).

Environment

  • Client versions observed: 2.1.199, 2.1.196, 2.1.179
  • CLAUDE_CODE_RETRY_WATCHDOG=1 and CLAUDE_CODE_MAX_RETRIES=15 are set and confirmed active (retry counter shows /16)
  • macOS (Apple Silicon), installed via Homebrew cask; Windows PC unaffected
  • Model: claude-opus-4-8[1m]

x-client-request-id samples for server-log lookup

  • 40bad3a9-c066-4e2a-8bf5-5ad34846cc0e (client flagged this one as "give this to the API team for server-log lookup")
  • cebf6335-0267-4259-81c4-9d8652d40c74
  • a748c52a-58ea-4b23-b493-dd83da11e82b
  • 23efc0a0-40e1-4f11-b954-5171f5bc16ad
  • fbd1203c-dded-4b1f-baf0-c3defaa89659
  • 850cd93f-014c-4db6-a365-0c498f2fb7eb

Timestamps: 2026-07-03 05:27–05:30 UTC and 07:14–07:15 UTC.
Source contexts: repl_main_thread and away_summary (forked agent).

Question

Can the team look up these request IDs server-side to determine where the connection is being reset (edge/LB/backend)? Happy to provide full debug logs.

View original on GitHub ↗

3 Comments

nicky-dev · 1 month ago

Root cause found — IPv4 edge resets requests with body ≥ ~2KB; IPv6 edge is fine

Reproduced deterministically on an affected machine (macOS, Apple Silicon). The ECONNRESET is not auth, token, MTU-in-general, or a local proxy — it is specific to the IPv4 edge of api.anthropic.com resetting POST requests once the request body exceeds ~1 TCP segment (~2KB).

Evidence

Current DNS for api.anthropic.com:

  • A: 160.79.104.10
  • AAAA: 2607:6bc0::10

Large POST (500KB body) to /v1/messages, 10 iterations each:

| Path | Result |
|------|--------|
| IPv4 160.79.104.10 | reset 10/10 (curl exit 56 / ECONNRESET, ~40ms) |
| IPv6 2607:6bc0::10 | 200/401 OK 10/10 |

Body-size threshold sweep over IPv4 (5x each):

body {} (2 bytes)  : OK (401)
body ~2 KB         : reset 5/5
body ~8 KB         : reset 5/5
body ~32 KB        : reset 5/5
... every size ≥2KB: reset 5/5

curl -v on the failing IPv4 request shows: TCP connects to 160.79.104.10:443, TLSv1.3 handshake completes, HTTP/2 stream opens, :method: POST is sent, then the connection is reset as the body is transmitted. Cert validates (real edge, no MITM).

What it is NOT

  • Not the machine/network MTU: large IPv4 POST to www.google.com (405) and cloudflare.com (301) succeed fine at the same time.
  • Not a local proxy/redirect: no PF rdr rules for :443; curl connects directly to the real edge IP with a valid cert.
  • Not auth: OAuth token check succeeds on every attempt; a 2-byte body returns 401 normally over IPv4.

Why it manifests as intermittent ECONNRESET in the client

Real Claude Code requests always carry >2KB (system prompt + tool defs + context), so they always exceed the threshold. The bundled runtime's Happy Eyeballs selects IPv4 because the IPv4 TCP connect succeeds (and TLS completes) — the reset only happens later when the request body is sent — so the client keeps choosing the broken path and dies mid-request. A Windows machine on the same network/WiFi is unaffected (appears to use IPv6 / a different edge).

Simple client-side IPv6 forcing does not stick: an /etc/hosts AAAA entry gets merged with the DNS A record, and --dns-result-order=ipv6first is not honored by fetch, so the runtime still picks the IPv4 edge.

Ask

Please investigate the IPv4 edge 160.79.104.10 (and the surrounding prefix) resetting /v1/messages POST requests whose body exceeds ~1 segment. This may be a broken edge node or a path/MTU issue on the IPv4 prefix. Happy to run any further diagnostics or provide packet captures.

nicky-dev · 1 month ago

Follow-up: IPv6 workaround confirmed end-to-end + cross-platform data point

Workaround verified. On all three affected macOS machines, forcing traffic onto IPv6 (blackholing Anthropic's IPv4 /21 with a reject route so the client falls back to IPv6) makes real claude -p requests succeed reliably — no ECONNRESET, no retries. This confirms the failure is on the IPv4 path/edge specifically, not the client or general connectivity.

sudo route -n add -net 160.79.104.0/21 -interface lo0 -reject
# claude -p now returns {"subtype":"success","is_error":false,...}

Cross-platform observation (may help localize it). A Windows 11 machine on the same LAN / same router / same ISP, reaching the same IPv4 edge 160.79.104.10 directly (no VPN, source on the same subnet), does NOT reproduce the reset — large IPv4 POSTs return 401 normally, 5/5. The three macOS machines on that identical path reset 100% of the time on the same request.

So on one physical network:

  • macOS → IPv4 large POST to 160.79.104.10 = reset (after TLS+HTTP/2, as body is sent)
  • Windows → IPv4 large POST to 160.79.104.10 = works
  • Both → IPv6 edge 2607:6bc0::10 = works
  • Both → large POST to google/cloudflare over IPv4 = works

Same path, same destination, differs only by client OS. This points to the IPv4 edge (or a middlebox on the path to it) mishandling something specific to macOS's large TCP segments / full-MTU packets rather than a plain edge outage. Windows likely also uses IPv6 preferentially, which is why users on Windows see this only rarely.

Happy to provide a pcap from a resetting macOS request vs a working Windows one if that helps.

weekdaysailor · 1 month ago

Another affected setup, with request IDs for the server-side lookup this issue asks for — plus evidence that the failing requests hit a warm prompt cache, which rules out the cold-cache mechanism proposed in #79989 / #74544.

Same symptom as reported here: intermittent API Error: Unable to connect to API (ECONNRESET), transport-level, macOS, auth fine throughout, retries exhaust and abort the turn.

One difference worth stating up front: this issue's onset is 2026-07-03 on CC 2.1.179–2.1.199, mine is 2026-07-23 on 2.1.217–2.1.220. Same signature, later onset — so either it's long-lived and intermittent per-account, or these are two instances of the same edge behavior.

Scope

21 ECONNRESET events (filtered on Claude Code's isApiErrorMessage flag — a naive grep over transcripts inflates the count, because diagnostic sessions echo the string into their own logs) out of 15,976 API requests across 6 projects, 2026-07-07 → 2026-07-27, all counted by one script.

  • 13,181 requests before the first event (2026-07-23T22:59Z) — zero errors.
  • 2,795 requests since — 21 errors (0.75%).
  • Of the 21, 8 are distinct first failures; the other 13 are follow-on failures inside an already-running retry loop.

Still occurring as of today.

Key finding: the prompt cache was warm on every failure

| timestamp (UTC) | CC ver | model | cache_read | total ctx | cache share | last successful requestId |
|---|---|---|---|---|---|---|
| 2026-07-23T22:59:49 | 2.1.217 | claude-fable-5 | 271,765 | 272,762 | 99.6% | req_011CdKp27ZPZPeVkco4iC7Gk |
| 2026-07-23T23:35:34 | 2.1.217 | claude-fable-5 | 312,357 | 315,398 | 99.0% | req_011CdKrhnXFviRAMkGYvQ6N2 |
| 2026-07-24T21:44:17 | 2.1.218 | claude-fable-5 | 494,119 | 501,807 | 98.5% | req_011CdMbxEyss4TG7gw87Xf12 |
| 2026-07-25T02:12:22 | 2.1.218 | claude-opus-4-8 | 189,260 | 194,868 | 97.1% | req_011CdMxXHQUj2Cv3X9DZK3SX |
| 2026-07-25T02:47:49 | 2.1.220 | claude-opus-5 | 107,740 | 108,729 | 99.1% | req_011CdN1DBpU2nvV31AcoKyQd |
| 2026-07-25T03:08:47 | 2.1.220 | claude-opus-5 | 136,792 | 137,087 | 99.8% | req_011CdN2pGWqjFFGst3qKEX5S |
| 2026-07-26T18:57:04 | 2.1.220 | claude-opus-5 | 553,882 | 556,964 | 99.4% | req_011CdRAwB2L73nGdGmiriyJz |
| 2026-07-27T02:05:16 | 2.1.220 | claude-opus-5 | 524,722 | 525,142 | 99.9% | req_011CdRjWyp4VFAPFPwxLpTxM |

requestId is from the last successful request immediately before each failure — the reset request itself never returns an ID. The failing request is the next one in the same conversation, ~6 minutes later.

On every failure that preceding request read 97.1%–99.9% of its context from cache, with input_tokens: 2 and cache_creation_input_tokens of only 293–7,686. The failing request therefore put a few KB of new tokens on the wire against a warm cache — not a large uncached re-ingestion.

That's the substantive disconfirmation of #79989's mechanism ("the first fully-uncached replay of a big context is what fails"). Here the cache is warm, the incremental payload is tiny, and the resets land mid-conversation during continuous work rather than after an idle gap.

On the >200k threshold claim

#79989 states "every observed failure is >200k and we have never observed a failure ≤200k". Two of mine are below that in live context size — 108,729 and 137,087 tokens — plus one at 194,868.

Stated precisely, because it matters: Claude Code's transcript usage entries record the base model ID with any [1m] suffix stripped, so I cannot tell from my logs whether the 1M-context beta header was set on those requests. Other sessions in the same account reached 501k–557k, so the beta is clearly in use on this account generally. A "those were on the long-context path regardless of size" reading is not excluded by my data. I'd treat the size counterexamples as suggestive only — the cache warmth, not the size, is what actually contradicts the proposed mechanism.

Retry budget: fixed ~350s wall clock, independent of payload

Measured from the user's prompt to the surfaced error (not from the previous assistant turn, so it excludes think time):

342, 348, 352, 352, 357, 358, 378, 351  seconds

Every failure aborts 5.7–6.3 minutes after the prompt is sent, and the spread is the same at 108k context as at 557k. So the retry window is bounded by wall clock, not by payload size or attempt cost.

What does not correlate: session age

I checked this because "long-running sessions" was my initial hypothesis, and my own logs don't support it. Session age at failure:

0.2h, 0.6h, 3.5h, 5.2h, 5.8h, 14.2h, 18.7h, 28.6h

Two failures landed 12 and 36 minutes into a fresh session. Meanwhile 59.7% of all 15,976 successful requests were issued at a session age above the minimum failing age, and successful requests run out to a 59.4h session age. Session longevity is not the driver — posting the negative result so nobody else chases it.

Context size doesn't separate the groups either: failures span 108k–557k, while the median across all requests is 321,774 tokens (p90 772,398, max 1,793,423). Far larger requests succeed routinely.

Environment

  • Claude Code 2.1.217 – 2.1.220 (native binary), macOS Darwin 25.3.0, Apple Silicon
  • Models: claude-fable-5, claude-opus-4-8, claude-opus-5 — all three affected
  • OAuth (Max), no ANTHROPIC_BASE_URL, no proxy (scutil --proxy empty), direct Wi-Fi to gateway. Tailscale installed but not on the API path, no exit node.

Ruled out locally, with evidence

  • Published outage — no incident overlaps the window (the incident cluster is 07-20→07-22, before onset). Rules out published incidents only; a client-visible reset during an unpublished capacity event wouldn't show up.
  • Local link flapping/usr/bin/log show across the error window: zero LinkDown, zero link-quality-degraded, zero nw_path unsatisfied, covering 10 of the error moments. Retention verified by narrow probes at each moment returning 30k–63k real log lines, so the zero isn't vacuous. (An earlier "no flaps" result in my own notes was withdrawn and re-run — log had been shadowed by a shell function and silently returned nothing.)
  • TCP stacknetstat -s: 0 bad resets, 0 retransmit-timeout drops, 0 keepalive-timeout drops in 53 days of uptime.
  • Local concurrency — the project holding most of the errors also runs a separate multi-vendor LLM harness. Checked run-by-run: zero temporal overlap between its 25 timestamped run windows and the error timestamps.

Ask

Same as the original request in this issue: can someone look up those request IDs server-side and identify where the connection is being reset — edge, LB, or backend? Each listed ID is the last successful request in its conversation; the reset is on the immediately following request in the same session, within ~6 minutes.

Happy to provide full transcripts or run instrumented captures.