Sessions become unresponsive zombies after idle TCP connections die silently

Resolved 💬 1 comment Opened Feb 15, 2026 by evanreiser Closed Feb 15, 2026

Summary

Claude Code sessions running in tmux become completely unresponsive after approximately 3 hours of idle time. The TUI continues to render (status bar visible, prompt shown) but accepts no input. The root cause is silent TCP connection death combined with a CLOSE_WAIT socket leak: the API server closes idle connections, but Claude Code never cleans up the dead sockets or reconnects.

Symptoms

  • Long-running Claude Code processes stop responding to keystrokes after ~3 hours idle
  • The terminal UI still renders normally — status bar updates, prompt is visible
  • No error message is displayed; the session appears functional but is a zombie
  • Only affects idle sessions; sessions with recent activity remain healthy
  • Keystrokes typed during the "dead" period accumulate in the PTY buffer and appear in shell history after killing the process — confirming input reaches the OS but Claude Code's event loop is stuck
  • Killing and restarting the process is the only recovery path

Diagnosis

Every unresponsive process has exactly zero TCP connections. Every responsive process has at least one.

| PID | Uptime | TCP Connections | FDs | Status |
|-----|--------|----------------|-----|--------|
| 66541 | 2h 48m | 3 | 79 | Alive |
| 51446 | 3h 12m | 6 | 110 | Alive |
| 42413 | 3h 18m | 3 | 104 | Alive |
| 52338 | 3h 12m | 1 | 113 | Alive |
| 52465 | 3h 12m | 1 | 111 | Alive |
| 51123 | 3h 13m | 0 | 101 | Dead |
| 51203 | 3h 13m | 0 | 101 | Dead |
| 88683 | 12h | 0 | 195 | Dead |
| 65286 | 23h | 0 | 840 | Dead |
| 51890 | 20 days | 0 | 96 | Dead |
| 69904 | 11h | 0 | 105 | Dead |

FD count does not predict failure — PID 51890 ran for 20 days with only 96 FDs but died with 0 TCP connections.

CLOSE_WAIT Socket Leak

Additionally, every running Claude process accumulates ~30 CLOSE_WAIT sockets:

| PID | Uptime | ESTABLISHED | CLOSE_WAIT |
|-----|--------|-------------|------------|
| 42413 | 3h 55m | 61 | 31 |
| 52338 | 3h 50m | 61 | 30 |
| 52465 | 3h 50m | 64 | 29 |
| 61289 | 19m | 59 | 29 |

CLOSE_WAIT means the server sent a FIN to close the connection, the OS acknowledged it, but Claude Code never called close() on its socket. These leaked sockets accumulate over time.

Root Cause Analysis

  1. Server-side idle timeout. The API endpoint (likely behind Cloudflare) closes idle TCP connections after 1-4 hours by sending a FIN.
  1. CLOSE_WAIT socket leak. Claude Code does not handle the socket close/end events. Instead of cleaning up dead connections, they accumulate in CLOSE_WAIT state (~30 per process).
  1. No TCP keepalive probes. macOS defaults net.inet.tcp.always_keepalive to 0, so idle connections get no probes. Without probes, the client never discovers connections the CDN silently dropped (as opposed to clean FIN closures).
  1. No request timeout or reconnection. When all pooled connections are dead and the user triggers an API call, the HTTP request is sent on a dead socket. No timeout fires, no error is surfaced. The Node.js event loop hangs waiting for a response that will never arrive.
  1. Silent zombie state. The TUI freezes because rendering is gated on the API response. Keystrokes accumulate in the kernel PTY buffer (confirmed by their appearance in shell history after killing the process). The session is permanently stuck with no user-visible error.

Suggested Fixes

1. Handle socket close/end events

Properly listen for close, end, and error events on pooled HTTP sockets. When the server sends a FIN, close the local socket and remove it from the connection pool. This would eliminate the CLOSE_WAIT leak.

2. Enable SO_KEEPALIVE on TCP sockets

const agent = new https.Agent({
  keepAlive: true,
  keepAliveMsecs: 60_000,
});

3. Add request timeouts

Set a reasonable timeout on API requests so that a dead connection causes a retry on a fresh socket rather than an infinite hang.

4. Detect and recover from dead connections

When a request fails (EPIPE, ECONNRESET, timeout), destroy the dead socket, retry on a fresh connection, and if reconnection fails, display a clear "Connection lost" error rather than silently hanging.

Workaround

Enabling system-wide TCP keepalives on macOS mitigates silent connection death:

sudo sysctl -w net.inet.tcp.always_keepalive=1
sudo sysctl -w net.inet.tcp.keepidle=300000

This does not fix the CLOSE_WAIT leak but prevents silent connection death for connections that never received a FIN.

Environment

  • OS: macOS 15 (Sequoia)
  • Claude Code: v2.1.42
  • Terminal: tmux sessions (long-running)
  • Reproduction rate: ~100% for sessions idle longer than 3 hours

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗