Claude Code CLI: GC Death Spiral in Heap Helper thread during idle long-running session (40h, 13GB, M4 Mac)

Resolved 💬 4 comments Opened Feb 8, 2026 by gagarinyury Closed Mar 9, 2026

Observed

Claude Code CLI process consumes ~263–287% CPU (≈3 cores) and ~12.9 GB RAM after 40+ hour session.

Root cause pinpointed: Heap Helper Thread stuck in GC operations (98% of samples), not Bun worker threads.

Thread_273493: Heap Helper Thread  
3965/4046 samples (98%) in heap/GC operations
Addresses: 0x1660678 → 0x2034a64 (allocator/GC code path)

Key observations:

  • Main thread healthy (in event loop, kevent64)
  • All 10 Bun Pool workers sleeping (__ulock_wait2)
  • 1 Heap Helper thread pegged at ~98% CPU ✅ smoking gun
  • No network activity (no TCP connections)
  • Minimal file handles (27 total, no file scanning)
  • CPU remains high even without interaction for 28+ hours

Causality proven experimentally:

SIGSTOP test:
Before freeze: CPU 43% user, 42% idle, load 6.11
During freeze: CPU 19% user, 65% idle, load 5.21  (↓57% user, ↑56% idle)
After resume:  CPU 40% user, 35% idle              (returns to high)

Environment

Hardware: Apple M4 (ARM64) ⚠️
Power: Battery mode (5% remaining, discharging at detection time)
Thermal: Potential throttling during long session (not confirmed)

OS: macOS 26.2 (arm64)
Shell: zsh
Claude: v2.1.34 binary (v2.1.37 installed via package manager)
Command: claude --dangerously-skip-permissions  
Working dir: /Users/yurygagarin/code/Yousing
.claude dir size: 83M (no large .claude.json, only 8KB sessions-index)

Session timeline:

Start: Feb 7, 07:30
Last user interaction (file edit): Feb 8, 19:51 (~12h into session)
Runaway detected: Feb 9, 00:20 (~40h total, ~28h idle)

Workload context:

Project: Karaoke app (audio processing, Demucs, Flask)
Repo size:
  - Source files: ~60 (TS/JS/PY)
  - LOC: ~4,400
  - node_modules: 195M
  - Audio files: 37 (large binary assets)
  - No massive monorepo, moderate complexity

Memory Growth Timeline

Estimated (no exact logs, based on patterns):

Start (~0h):     ~600 MB  (typical Claude CLI baseline)
+6h:            ~2.1 GB  (linear accumulation)
+12h:           ~5.4 GB  (last interaction point)
+24h:           ~9.7 GB  (idle growth continues)
+40h:          ~12.9 GB  (runaway detected)

Growth rate: ~320 MB/hour sustained
Pattern: Linear accumulation, no spike

→ Indicates memory leak, not burst allocation

Activity State When Runaway Begins

IDLE SESSION RUNAWAY (critical classification):

  • Last code generation: ~Feb 8, 19:51
  • Runaway detected: ~Feb 9, 00:20
  • Time between last interaction and detection: ~28 hours
  • Process left open but unused
  • No active code generation or user input

This is NOT:

  • Active generation runaway
  • Indexing spike
  • User-triggered load

This IS:

  • Background memory leak during idle
  • GC unable to reclaim accumulated memory
  • Eventually triggers GC death spiral

Impact

System-wide degradation:

  • 3 CPU cores saturated (~25% total system CPU consumption)
  • Load average sustained at 6.0+ (normal: ~1-2)
  • System idle CPU: 42% → should be 80%+ on idle Mac
  • 12.9 GB memory consumed
  • Battery drain accelerated (5% remaining at detection)

Silent failure mode:

  • No error messages in stdout/stderr
  • No timeout mechanism
  • No progress indication
  • No memory pressure warnings
  • Process appears "alive" (responds to signals) but stuck

User experience:

  • System laggy/unresponsive
  • Fans spinning (thermal load)
  • No indication Claude is the cause
  • Must manually discover via Activity Monitor

Hypothesis

GC Death Spiral triggered by long-running idle session:

  1. Phase 1 (0-12h): Normal operation
  • Memory accumulates from session context/history
  • GC runs periodically, keeps up with allocation
  1. Phase 2 (12-28h): Idle memory leak
  • No user activity, but memory keeps growing
  • Possible retained references to large objects (audio buffers?)
  • GC cannot reclaim (objects still reachable)
  1. Phase 3 (28-40h): GC thrashing
  • Memory pressure threshold exceeded (~10-12 GB)
  • GC triggered frequently
  • Unable to free sufficient memory
  • Enters "death spiral": GC runs continuously, no progress
  • Heap Helper thread pegged at 98% CPU

Similar patterns in other runtimes:

  • JVM: "GC overhead limit exceeded"
  • V8/Node: GC pause storms
  • Python: Reference cycle accumulation

Potential root causes:

  • Unreleased references to session context/history
  • Audio buffer retention (project has 37 audio files)
  • AST/parse tree accumulation
  • WebAssembly memory not released (Bun internal)
  • Circular references preventing collection

Expected Behaviour

Long-running sessions should either:

  • Stabilize memory usage after initial ramp-up
  • Surface warnings when approaching memory limits (e.g., "Session using 10GB, recommend restart")
  • Implement backpressure (reject new work, throttle operations)
  • Degrade gracefully rather than saturate CPU indefinitely
  • Max GC time budget (e.g., fail after 60s of continuous GC)
  • Session auto-save + restart suggestion
  • Heap dump + exit (for debugging)

NOT: Silent infinite CPU consumption during idle

Reproduction Context

Cannot yet provide minimal repro - occurred organically after extended real-world usage.

Pattern suggests:

  • Idle session duration threshold (~24-30h)
  • Memory accumulation threshold (~10-12 GB)
  • Project with binary assets (audio files)
  • Apple Silicon (ARM-specific GC behavior?)

Potential repro approach (if you can suggest):

  1. Start Claude in audio processing project
  2. Perform normal work for ~12h
  3. Leave session idle (no interaction) for 24-30h
  4. Monitor memory growth
  5. Observe GC runaway when threshold reached

Artifacts

Available now:

  • ps -M output (all thread states)
  • sample 5-second profile (4046 samples, Heap Helper thread at 98%)
  • lsof output (27 file handles)
  • ✅ SIGSTOP/CONT experiment results (CPU impact proven)

No heap snapshot available - can attempt capture if recommended flags exist.

Sample excerpt (hot path):

Thread_273493: Heap Helper Thread
4046 samples total
3965 samples (98%) in:
  0x1660678 (heap operation entry)
  → 0x16766f0
  → 0x20129d4
  → 0x2034a64 (allocator/GC core)
  → 0x2036af0 (GC sweep/mark?)
  → 0x24fbb40 (repeated in loop)

Questions for Maintainers

  1. Known issue?
  • Is GC runaway in long idle sessions a known class of bugs in 2.1.34–2.1.37?
  • Any ARM-specific (Apple Silicon) GC issues?
  • Battery mode GC behavior differences?
  1. Debug instrumentation:
  • Flags to enable GC/heap logging?
  • Memory limit enforcement?
  • Heap profiling in production builds?
  • GC timeout/budget configuration?
  1. Mitigation scope:
  • Session max duration / idle timeout?
  • Memory watchdog (warn at N GB)?
  • GC time budget (fail-fast vs infinite loop)?
  • Automatic heap snapshots on runaway?
  • Graceful degradation strategy?
  1. Investigation assistance:
  • Should I keep process alive for additional profiling?
  • Specific flags/builds you'd like me to test?
  • Core dump useful? (13 GB process, will be large)

Workaround

Temporary (system relief):

  • SIGSTOP pauses process (proven to free CPU)
  • Loses interactivity, but system remains usable

Recommended user workaround:

  • Restart Claude every ~12-24h for long sessions
  • Monitor memory usage (Activity Monitor)
  • Avoid leaving sessions idle overnight on battery

Ideal mitigation:

  • Detect GC thrashing (e.g., >90% CPU in GC for >60s)
  • Log warning: "Session memory critical (12GB), GC struggling"
  • Offer graceful restart: "Save state and restart? [Y/n]"

---

Happy to run additional instrumentation if you suggest flags or builds.
Can keep process alive for deeper profiling if helpful.

View original on GitHub ↗

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