Headless --print --output-format=stream-json mode hangs indefinitely on streaming response

Resolved 💬 3 comments Opened Apr 26, 2026 by RodrigueFr Closed Apr 26, 2026

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:

  1. User sends a message
  2. Runner writes the user_input on stdin of the claude subprocess
  3. Subprocess sends request to api.anthropic.com/v1/messages (streaming)
  4. A few stream events arrive (partial messages / keepalives) — visible in stdout
  5. Stream then freezes for tens of minutes / indefinitely:
  • Subprocess in S state, CPU ~0
  • TCP connections to Anthropic still ESTABLISHED
  • No bytes received on the socket
  • No error logged anywhere
  1. 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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗