Subagent processes hang on exit due to OTEL telemetry export retries (429 rate limiting)

Resolved 💬 3 comments Opened Mar 3, 2026 by Yossarian151 Closed Mar 7, 2026

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

  1. Run a Claude Code session that spawns multiple concurrent subagents (e.g., 3+ Agent tool calls with run_in_background)
  2. Wait for all agents to complete their work and return results
  3. 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:

  1. Subagent completes inference, returns result to parent process
  2. Node.js process begins graceful shutdown
  3. OTEL batch span processor attempts to flush pending telemetry events
  4. Telemetry endpoint returns 429 (rate-limited)
  5. Exporter retries with backoff — indefinitely
  6. 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.

View original on GitHub ↗

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