Headless --print --output-format=stream-json mode hangs indefinitely on streaming response
Summary
When running Claude Code in headless mode (claude --print --output-format=stream-json --input-format=stream-json --resume <session_id>) under stable network conditions, the subprocess can hang indefinitely after starting to receive a streamed response. The process stays in S (sleeping) state, consumes ~0% CPU, holds open TCP connections to api.anthropic.com but no further bytes flow. There is no client-side read timeout — the only recovery is to kill the PID manually.
The interactive TUI mode (claude / claude resume) is not affected by this bug.
Reproduction
Difficult to reproduce on demand — observed sporadically (a few times per day) on a long-running headless deployment with multiple concurrent sessions.
Setup:
- Long-running runner that spawns
claude --print --output-format=stream-json --input-format=stream-json --verbose --include-partial-messages --include-hook-events --permission-mode bypassPermissions --setting-sources=user,project,local --model claude-opus-4-7 --effort high --resume <session_id>per conversation - Each conversation has accumulated context (some sessions are several MB of jsonl)
- 3-5 concurrent sessions across different conversations
Pattern observed:
- User sends a message
- Runner writes the user_input on stdin of the claude subprocess
- Subprocess sends request to
api.anthropic.com/v1/messages(streaming) - A few stream events arrive (partial messages / keepalives) — visible in stdout
- Stream then freezes for tens of minutes / indefinitely:
- Subprocess in
Sstate, CPU ~0 - TCP connections to Anthropic still ESTABLISHED
- No bytes received on the socket
- No error logged anywhere
- Subsequent user_inputs queue up but never get processed
Workaround
External watchdog that:
- Tracks "significant" events only (
assistant,tool_use,tool_result,result) — partials/keepalives don'''t count - Kills the subprocess after N minutes of no significant event (10 min in our case)
- Hard timeout at 20 min per turn regardless of stream activity
- Runner respawns with
--resume <session_id>on next user message
This works but masks the underlying bug.
What we'''d expect
The headless SDK should expose a client-side read timeout (configurable via flag or env var, e.g. CLAUDE_CLIENT_READ_TIMEOUT) that aborts a streaming request if no data is received for N seconds. Or, ideally, a heartbeat/keepalive mechanism that detects dead streams server-side.
Environment
- Claude Code: 2.1.119
- Platform: Ubuntu 24.04, Linux 6.8
- Node bundled binary (npm-global install)
- Subprocess parent: Python
asyncio.create_subprocess_exec, drains stdout via line reader - Same bug also reproduces with
claude_agent_sdk(Python 0.1.68) which embeds the same binary
Notes
The interactive TUI mode never exhibits this. We'''re running 4-5 long-lived headless sessions per day for the past few weeks; in the same period, equivalent CLI usage in the terminal has had zero hangs. The --print --stream-json code path appears to have a different (and missing) read-timeout policy compared to the TUI.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗