[Bug] Agent-tool streams silently killed on idle-intolerant NAT paths — measured 4–5 min drop threshold; TCP keepalive on the socket demonstrably prevents it

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 0 comments · opened Jul 31, 2026

Summary

Controlled measurements that corroborate and extend #70017 and #81993: subagent (Agent-tool) dispatches die with Agent stalled: no progress for 600s (stream watchdog did not recover) because a NAT middlebox on the path silently evicts TCP connections that go bidirectionally silent for more than 4–5 minutes — exactly what an SSE stream does during long model reasoning. I probed the actual threshold, reproduced it twice from two environments on the same machine, and verified that enabling TCP keepalive on the socket prevents the failure mode entirely on the same path.

Environment

  • Claude Code CLI 2.1.220 (bun binary), npm install
  • Windows 11 build 26200 + WSL2 (Ubuntu, kernel 6.6.114.1), networkingMode=nat
  • Consumer ISP in Indonesia (CGNAT likely); reproduced identically from both WSL2 and native Windows (PowerShell), so this is not WSL-specific — matching #81993 which reports the same failure class on macOS

Method

TLS connections to api.anthropic.com:443 (single pinned IP), warmed with one HTTP/1.1 keep-alive request/response, then held idle for N minutes, then a second request. Outcome classes: ALIVE, CLOSED_DURING_IDLE (FIN/RST arrived during idle — client is informed), RST_ON_SEND, SILENT_DROP (send buffered, zero bytes back until 60s timeout). Two runs per environment; threshold accepted only when reproduced.

Results

| Idle | WSL plain | WSL + SO_KEEPALIVE (60s/15s/4) | Windows plain |
|---|---|---|---|
| 2–4 min | ALIVE | ALIVE | ALIVE |
| 5 min | SILENT_DROP (×2 runs) | ALIVE | SILENT_DROP (×2 runs) |
| 8–20 min | SILENT_DROP (all) | CLOSED_DURING_IDLE — FIN arrives | SILENT_DROP (all) |

  • ss -tnoi on a dropped socket: unacked:1 retrans:1/8 backoff:7-8 — the client retransmits into a void and is never told the connection is gone.
  • The keepalive variant proves two distinct benefits: (1) keepalive traffic keeps the NAT entry alive through silent spans; (2) when the server later closes the idle connection (its own ~5–8 min idle policy), the FIN actually reaches the client — a clean, immediately retryable close instead of an indefinite hang.

Impact data (from my subagent transcripts, July 2026)

  • 217 subagent runs in July; 55 (25%) contain inter-event gaps ≥590s.
  • Exact-600s gap signatures (watchdog kill + respawn) appear from July 3 onward in nearly every subagent session; worst observed gap 21,352s (repeated respawn cycles).
  • Context size correlates only probabilistically (bigger context → longer silent reasoning spans → higher chance of crossing the 4–5 min threshold): I have a 229k-token subagent with zero long gaps and a 72k one with a 21,352s gap.

Additional observations

  • Inspecting live sockets of the claude process: some show per-socket keepalive timers (sub-second countdown, i.e., setKeepAlive with a short initial delay), others show no keepalive timer at all. The failing path appears to be the one without keepalive.
  • The stream watchdog (CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MS, default 600000) defers while tools are in flight, so deaths always appear "right after a tool result" — during the next request's silent reasoning span.
  • Per #70017, the foreground stale-connection retry path does not cover Agent-tool dispatches, which is why interactive sessions survive the same network while subagents die.

Requests

  1. Enable SO_KEEPALIVE (~60s) on the API streaming sockets. Measured on this path: it converts indefinite silent hangs into either survival or a clean server FIN. This is a small client-side change that fixes the failure class for NAT'd users regardless of server behavior.
  2. SSE heartbeat during long reasoning spans (the #70017 ask) — prevention at the source, helps every consumer.
  3. A retry/resume path for Agent-tool dispatches equivalent to the foreground stale-connection retry, so one dropped connection doesn't kill a long-running subagent terminally.

Probe scripts (Python stdlib + PowerShell) and raw JSONL results available on request.

View original on GitHub ↗