Preflight Checklist
What's Wrong?
Claude Code hangs permanently when the API streaming response stalls mid-delivery. The process stays alive (epoll_wait in kernel) but makes no progress. The UI shows a spinner ("Accomplishing…",
"Ruminating…", etc.) indefinitely. No error is surfaced. The session cannot recover — the only fix is kill -9.
Two distinct hang patterns observed:
Pattern 1 — Mid-turn stream freeze: The API response starts streaming (thinking block received, "thought for 3s" renders), then the stream silently stops. No more tokens arrive. The JSONL session
log shows no new entries after the last progress update. The process is stuck waiting for bytes that never come.
Pattern 2 — Tool result delivery stall: A Bash tool executes and completes (e.g., curl --max-time 5), but the tool result is never delivered back to the conversation state. The last tool_use ID in
the JSONL has no matching tool_result. The UI shows the spinner as if the tool is still running.
Both patterns correlate with background agent (Task with run_in_background: true) notifications arriving between or during turns.
What Should Happen?
- Claude Code should have a read timeout on the SSE/streaming HTTP response. If no data arrives within N seconds (e.g., 60–120s), it should abort the request and surface a retryable error to the
user.
- Tool result delivery should have a timeout. If a tool completes but the result isn't consumed within N seconds, the session should error rather than hang.
- The UI should detect "no progress for N seconds" and offer the user an escape (e.g., "Session appears stuck. Press Enter to retry or Esc to cancel").
Error Messages/Logs
Session 1: Mid-turn stream freeze (fff87be2)
JSONL timeline:
21:43:28 UTC Turn 1 ends (system/turn_duration logged)
21:43:28 Background agent notification injected as user message
→ Triggers Turn 2
21:44:44 Turn 2 ends (system/turn_duration logged, stop hooks run)
Background agent notification for a DIFFERENT agent arrives
→ Triggers Turn 3
API call starts, "thought for 3s" renders in UI
*** No more JSONL entries. Stream froze. ***
UI stuck on "Accomplishing… (thought for 3s)" for 10+ minutes
Process state during hang:
$ ps -p 843072 -o pid,state,wchan
PID S WCHAN
843072 S do_epoll_wait
$ kill -TERM 843072 # no effect
$ kill -KILL 843072 # required to terminate
Session 2: Tool result delivery stall (06d50a72)
JSONL shows:
Last tool_use ID: toolu_vrtx_01JqgVUPZLrMyudEQaAQhx1x (Bash: curl)
Last tool_result ID: toolu_vrtx_01LJscxroh9R7tsDQUL6AhuF (different, earlier tool)
→ Mismatch: the curl's result was never delivered
The curl command had --max-time 5 so it completed, but the result never made it back to the conversation. UI showed "Ruminating…" indefinitely.
Steps to Reproduce
Difficult to reproduce deterministically — it appears to be a race condition or network-level issue. But the following pattern triggers it frequently:
- Start Claude Code on a remote Linux server via SSH + tmux
- Give it a complex task that spawns multiple background agents (Task with run_in_background: true)
- Wait for background agents to complete and deliver notifications
- When a notification arrives right as a turn is ending or between turns, the next API call has a high chance of hanging
Environment factors that may contribute:
- Remote server (high-latency network path to API)
- tmux (terminal multiplexing)
- Multiple concurrent Claude Code sessions in different tmux panes
- Heavy hook infrastructure (6 hook events, though all complete in <120ms per telemetry)
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.40
Claude Code Version
2.1.42
Platform
Google Vertex AI
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
Suggested fix
Add a read timeout to the HTTP streaming client. Pseudocode:
// In the SSE/streaming response handler:
const STREAM_READ_TIMEOUT_MS = 120_000; // 2 minutes
let lastDataTime = Date.now();
stream.on('data', (chunk) => {
lastDataTime = Date.now();
// ... process chunk
});
const watchdog = setInterval(() => {
if (Date.now() - lastDataTime > STREAM_READ_TIMEOUT_MS) {
stream.destroy(new Error('API stream read timeout'));
clearInterval(watchdog);
}
}, 10_000);
Workaround
External watchdog daemon that monitors JSONL session files and kills processes with no writes for >5 minutes. Available at user's request.
Preflight Checklist
What's Wrong?
Claude Code hangs permanently when the API streaming response stalls mid-delivery. The process stays alive (epoll_wait in kernel) but makes no progress. The UI shows a spinner ("Accomplishing…",
"Ruminating…", etc.) indefinitely. No error is surfaced. The session cannot recover — the only fix is kill -9.
Two distinct hang patterns observed:
Pattern 1 — Mid-turn stream freeze: The API response starts streaming (thinking block received, "thought for 3s" renders), then the stream silently stops. No more tokens arrive. The JSONL session
log shows no new entries after the last progress update. The process is stuck waiting for bytes that never come.
Pattern 2 — Tool result delivery stall: A Bash tool executes and completes (e.g., curl --max-time 5), but the tool result is never delivered back to the conversation state. The last tool_use ID in
the JSONL has no matching tool_result. The UI shows the spinner as if the tool is still running.
Both patterns correlate with background agent (Task with run_in_background: true) notifications arriving between or during turns.
What Should Happen?
user.
Error Messages/Logs
Session 1: Mid-turn stream freeze (fff87be2) JSONL timeline: 21:43:28 UTC Turn 1 ends (system/turn_duration logged) 21:43:28 Background agent notification injected as user message → Triggers Turn 2 21:44:44 Turn 2 ends (system/turn_duration logged, stop hooks run) Background agent notification for a DIFFERENT agent arrives → Triggers Turn 3 API call starts, "thought for 3s" renders in UI *** No more JSONL entries. Stream froze. *** UI stuck on "Accomplishing… (thought for 3s)" for 10+ minutes Process state during hang: $ ps -p 843072 -o pid,state,wchan PID S WCHAN 843072 S do_epoll_wait $ kill -TERM 843072 # no effect $ kill -KILL 843072 # required to terminate Session 2: Tool result delivery stall (06d50a72) JSONL shows: Last tool_use ID: toolu_vrtx_01JqgVUPZLrMyudEQaAQhx1x (Bash: curl) Last tool_result ID: toolu_vrtx_01LJscxroh9R7tsDQUL6AhuF (different, earlier tool) → Mismatch: the curl's result was never delivered The curl command had --max-time 5 so it completed, but the result never made it back to the conversation. UI showed "Ruminating…" indefinitely.Steps to Reproduce
Difficult to reproduce deterministically — it appears to be a race condition or network-level issue. But the following pattern triggers it frequently:
Environment factors that may contribute:
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.40
Claude Code Version
2.1.42
Platform
Google Vertex AI
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
Suggested fix
Add a read timeout to the HTTP streaming client. Pseudocode:
// In the SSE/streaming response handler:
const STREAM_READ_TIMEOUT_MS = 120_000; // 2 minutes
let lastDataTime = Date.now();
stream.on('data', (chunk) => {
lastDataTime = Date.now();
// ... process chunk
});
const watchdog = setInterval(() => {
if (Date.now() - lastDataTime > STREAM_READ_TIMEOUT_MS) {
stream.destroy(new Error('API stream read timeout'));
clearInterval(watchdog);
}
}, 10_000);
Workaround
External watchdog daemon that monitors JSONL session files and kills processes with no writes for >5 minutes. Available at user's request.