Subagent processes hang on exit due to OTEL telemetry export retries (429 rate limiting)
Description
Claude Code subagent processes (spawned by the Agent tool via --output-format stream-json) complete their inference work but hang indefinitely on exit while the OpenTelemetry exporter retries failed telemetry event exports against a rate-limited endpoint (HTTP 429).
Steps to Reproduce
- Run a Claude Code session that spawns multiple concurrent subagents (e.g., 3+ Agent tool calls with
run_in_background) - Wait for all agents to complete their work and return results
- Check for lingering processes:
ps aux | grep 'claude.*stream-json' | grep -v grep
The subagent processes remain alive indefinitely after completing their work.
Root Cause Analysis
From debug logs (~/.claude/debug/{session_id}.txt):
[ERROR] "Error: Error: 1P event logging: 37 events failed to export
(status=429, code=ERR_BAD_REQUEST, Request failed with status code 429)"
The shutdown sequence:
- Subagent completes inference, returns result to parent process
- Node.js process begins graceful shutdown
- OTEL batch span processor attempts to flush pending telemetry events
- Telemetry endpoint returns 429 (rate-limited)
- Exporter retries with backoff — indefinitely
- Process never exits because OTEL shutdown hook never resolves
With multiple concurrent subagents (observed: 14 from a single session), this becomes a thundering herd — all finish around the same time, all try to flush telemetry simultaneously, all get rate-limited, all hang.
Impact
- Each stuck process holds ~300-400 MB of RAM
- 14 stuck processes = 4.8 GB wasted
- Failed telemetry events accumulate on disk at
~/.claude/telemetry/1p_failed_events.{session_id}.{subagent_id}.json(observed: 27 MB across 19 files) - Processes persist until manually killed or machine reboots
Environment
- Claude Code version: 2.1.63
- Platform: macOS (Darwin 25.3.0, arm64)
- Node: v24.3.0 (running with Bun)
Suggested Fix
Add a hard timeout to the OTEL shutdown path for non-interactive (is_interactive: false) processes. Something like:
// On graceful shutdown of subagent processes
const OTEL_SHUTDOWN_TIMEOUT_MS = 5000; // 5 seconds max
await Promise.race([
otelExporter.shutdown(),
new Promise(resolve => setTimeout(resolve, OTEL_SHUTDOWN_TIMEOUT_MS))
]);
process.exit(0);
Alternatively, respect the standard OTEL_BSP_EXPORT_TIMEOUT environment variable, or simply skip telemetry flush for subagent processes entirely (the parent process can handle aggregate reporting).
Current Workaround
Periodic cleanup via hook:
# Kill orphaned subagent processes
pgrep -f 'claude.*--output-format stream-json' | xargs kill 2>/dev/null
# Remove failed telemetry files
find ~/.claude/telemetry -name '1p_failed_events.*' -delete
We run this as an async PostToolUse hook on Write|Edit events and on SessionStart.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗