Sessions become unresponsive zombies after idle TCP connections die silently
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
- Server-side idle timeout. The API endpoint (likely behind Cloudflare) closes idle TCP connections after 1-4 hours by sending a FIN.
- CLOSE_WAIT socket leak. Claude Code does not handle the socket
close/endevents. Instead of cleaning up dead connections, they accumulate in CLOSE_WAIT state (~30 per process).
- No TCP keepalive probes. macOS defaults
net.inet.tcp.always_keepaliveto0, so idle connections get no probes. Without probes, the client never discovers connections the CDN silently dropped (as opposed to clean FIN closures).
- 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.
- 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
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗