[Bug] HTTP/2 connection pool missing SO_KEEPALIVE, causing hangs on CGNAT networks

Resolved 💬 4 comments Opened Apr 12, 2026 by adigourdi Closed May 25, 2026

Bug Description

Claude Code pool connections lack SO_KEEPALIVE, causing silent hangs on CGNAT networks

Summary

Claude Code's pooled HTTPS connections to api.anthropic.com do not have the SO_KEEPALIVE socket option enabled. On networks behind carrier-grade NAT (CGNAT) — common on mobile data / tethered connections — this causes the following failure pattern:

  1. The HTTP/2 connection pool opens several long-lived sockets to Anthropic's edge.
  2. Because SO_KEEPALIVE is off, no TCP keepalive probes are sent during idle periods.
  3. The carrier's NAT translation table silently evicts the idle flow (often well below RFC 5382's recommended 2h4min established-state timeout — in practice a few minutes).
  4. From the kernel's perspective, the sockets remain ESTABLISHED.
  5. The next user message is written into one of these zombie sockets. The peer never sees it.
  6. Linux TCP retransmits into the void. With default tcp_retries2=15, this means up to ~15 minutes of hang before the socket dies.
  7. The client's retry logic then walks through the rest of the pooled zombies serially, each costing another ~15 min (or ~15s with tuned tcp_retries2=6).

User-visible effect: the first message after any idle period (even on the same network — no switch required) hangs for tens of seconds to many minutes. Cancelling and resending typically succeeds immediately because a fresh connection gets established.

Evidence

Captured with ss -tnoe 'dst :443' on Linux while Claude Code was running on a mobile tethered connection.

Other applications' sockets (Firefox, VS Code, gnome-terminal child processes) show a keepalive timer:

ESTAB 0 0 10.134.87.203:56958 150.171.109.83:443
  timer:(keepalive,6.145ms,0) ... cgroup:...app-gnome-code...
ESTAB 0 0 10.134.87.203:53086 34.107.243.93:443
  timer:(keepalive,3min18sec,0) ... cgroup:...app-gnome-firefox...
ESTAB 0 0 10.134.87.203:59916 31.13.83.51:443
  timer:(keepalive,56sec,0) ... cgroup:...app-gnome-firefox...

Claude Code's sockets to api.anthropic.com (160.79.104.10) show no timer field, confirming SO_KEEPALIVE is not set:

ESTAB 0 0 10.134.87.203:49994 160.79.104.10:443
  uid:1000 ino:2447964 sk:2003 cgroup:...vte-spawn-...  <->
ESTAB 0 0 10.134.87.203:60090 160.79.104.10:443
  uid:1000 ino:2452395 sk:2004 cgroup:...vte-spawn-...  <->

When a hang was actively reproduced, the stuck socket's send queue held ~96KB of request data with an active retransmit timer deep into the exponential-backoff schedule:

ESTAB 0 95895 10.134.87.203:53072 160.79.104.10:443
  timer:(on,1min2sec,8)

Reproduction

  1. Use Claude Code on a network behind CGNAT (most mobile carriers / tethered hotspots).
  2. Send a message so the pool warms up.
  3. Leave the session idle long enough for the carrier NAT to evict flows (varies; often 2–5 minutes).
  4. Send another message.

Expected: message sends promptly.
Actual: message hangs until either TCP tcp_retries2 gives up (default: up to ~15 min) or the user cancels and retries.

Diagnostic confirmation that kernel tuning alone is insufficient

On the affected host:

net.ipv4.tcp_retries2 = 6              # lowered from default 15
net.ipv4.tcp_keepalive_time = 60       # lowered from default 7200
net.ipv4.tcp_keepalive_intvl = 10      # lowered from default 75
net.ipv4.tcp_keepalive_probes = 3      # lowered from default 9
  • tcp_retries2=6 does help: zombie sockets now die in ~15s instead of ~15 min. Confirmed via watch ss -tno — retry counter climbs to 5–6 then the socket is reaped.
  • tcp_keepalive_* has no effect on Claude Code's sockets because it requires SO_KEEPALIVE to be set per-socket, which the client does not do.

Because the pool holds multiple zombies, a single user message can still stall for N × 15s while the client serially retries through each dead connection.

Proposed fix

Enable SO_KEEPALIVE on the HTTP/2 pool connections used by Claude Code's HTTP client (presumably undici / the Anthropic SDK's HTTP agent). Node.js exposes this via socket.setKeepAlive(true, initialDelayMs).

A reasonable default initial delay would be 30–60 seconds, which is short enough to beat most CGNAT idle timeouts while being light on traffic. The existing Linux sysctl defaults (tcp_keepalive_time=7200) are far too long for modern carrier environments and should be overridden at the application level.

Alternatively or additionally: send HTTP/2 PING frames on idle connections at a similar cadence. PING frames have the advantage of being an application-layer health check, so they detect not just dead TCP but also broken proxies/load-balancers along the path.

Impact

This affects any Claude Code user on:

  • Mobile data / tethered hotspots (very common when traveling).
  • Residential ISPs that deploy CGNAT (increasingly common globally as IPv4 exhaustion progresses).
  • Corporate networks with aggressive stateful firewalls.

For these users, Claude Code is currently unrel…
Note: Content was truncated.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗