Claude Code CLI: GC Death Spiral in Heap Helper thread during idle long-running session (40h, 13GB, M4 Mac)
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:
- Phase 1 (0-12h): Normal operation
- Memory accumulates from session context/history
- GC runs periodically, keeps up with allocation
- 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)
- 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):
- Start Claude in audio processing project
- Perform normal work for ~12h
- Leave session idle (no interaction) for 24-30h
- Monitor memory growth
- Observe GC runaway when threshold reached
Artifacts
Available now:
- ✅
ps -Moutput (all thread states) - ✅
sample5-second profile (4046 samples, Heap Helper thread at 98%) - ✅
lsofoutput (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
- 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?
- Debug instrumentation:
- Flags to enable GC/heap logging?
- Memory limit enforcement?
- Heap profiling in production builds?
- GC timeout/budget configuration?
- 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?
- 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.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗