[Bug] v2.1.25 Main Thread Busy-Wait: 75% of sessions spin at 60-70% CPU (WCHAN = -)

Resolved 💬 8 comments Opened Jan 30, 2026 by NonsuchNinja Closed Mar 2, 2026

Claude Code v2.1.25 Main Thread Busy-Wait CPU Bug

Summary

Claude Code v2.1.25 exhibits a critical bug where the main event loop thread enters an infinite busy-wait loop with no wait channel (WCHAN = -), consuming 60-70% CPU continuously without performing productive work. The bug triggers immediately at session start in approximately 75% of sessions (3 out of 4 observed).

Environment

  • OS: Ubuntu Linux 6.8.0-90-generic
  • Claude Code Version: 2.1.25 (224MB Bun executable)
  • Binary Location: /home/ceo/.local/share/claude/versions/2.1.25
  • Binary Hash: 696135f0eccaf7a4070168845146833fa4fc93a6191fe026a7517af4d2e14fec
  • System: Production server running 4 concurrent Claude Code sessions
  • Working Directory: /opt/NILAssist

Reproduction

Reproduction Rate: ~75% (3 out of 4 sessions affected immediately at startup)

Steps:

  1. Start multiple Claude Code sessions with claude --resume
  2. Observe CPU usage with ps aux | grep claude
  3. ~75% of sessions immediately spike to 60-70% CPU
  4. Bug persists indefinitely (observed for 90+ minutes)

Cannot be reliably reproduced - appears to be race condition or timing-dependent at session initialization.

Observed Behavior

Four Sessions Tested - Same Binary (v2.1.25)

PID 1623341:  0.6% CPU  ✓ NORMAL    (runtime: 1h 44m, CPU time: 40s)
PID 1628701: 69.6% CPU  ✗ AFFECTED  (runtime: 1h 28m, CPU time: 1h 1m)
PID 1652227: 60.9% CPU  ✗ AFFECTED  (runtime: 54m, CPU time: 33m)
PID 1667839: 42.4% CPU  ✗ AFFECTED  (runtime: 10m, CPU time: 4.2m)

Key Observation: Session 1667839 reached 58% CPU in only 45 seconds of runtime, proving the bug triggers at startup, not gradually.

Root Cause Analysis

Thread-Level Diagnosis

Normal Session (PID 1623341) - Main Thread:

LWP 1623341:  0.3% CPU, WCHAN = ep_poll  ✓ HEALTHY (sleeping on epoll)

Broken Session (PID 1628701) - Main Thread:

LWP 1628701: 62.6% CPU, WCHAN = -        ✗ BUSY-WAIT (no wait channel!)

Process State Comparison

Normal Session:

  • STAT: Sl+ (S = sleeping, interruptible)
  • WCHAN: ep_poll (blocked on epoll_wait syscall)
  • Context Switches: 69,952 total (42,754 voluntary / 27,198 nonvoluntary)
  • Threads: 11

Broken Session:

  • STAT: Rl+ (R = running/runnable - NOT sleeping!)
  • WCHAN: - (no wait channel - never blocks!)
  • Context Switches: 1,370,914 total (1,151,794 voluntary / 219,120 nonvoluntary)
  • 19.6x more context switches than normal
  • Threads: 13 (includes extra JITWorker thread not present in normal session)

Secondary Effects

HeapHelper Threads Also Burning CPU:

HeapHelper 1: 2.0% CPU
HeapHelper 2: 1.9% CPU
HeapHelper 3: 1.7% CPU
Total: 5.6% additional CPU

Suggests the busy-wait loop is constantly allocating objects, triggering continuous garbage collection.

Syscall Analysis (Inferred)

The main thread with WCHAN = - is not calling any blocking syscall. Based on issue #10493, the likely culprit is:

epoll_pwait(epfd, events, maxevents, timeout=0, sigmask)

With timeout=0, the syscall returns immediately instead of blocking, creating a tight polling loop:

while (true) {
  epoll_pwait(..., timeout=0);  // Returns immediately
  // Loop continues without sleeping
}

What We Tested

❌ Killed Playwright MCP Servers - NO EFFECT

Killed all Playwright MCP processes (4 node processes) for session 1652227:

  • Before: 60.9% CPU
  • After (2s): 60.9% CPU
  • After (5s): 60.9% CPU
  • After (10s): 60.9% CPU

Conclusion: MCP servers are NOT the cause. Bug is in Claude Code binary's event loop.

✓ Confirmed: Identical MCP Configuration

Both normal and broken sessions have identical MCP servers attached:

  1. PayPal MCP (@paypal/mcp)
  2. Codex MCP (node /usr/bin/codex mcp-server)
  3. AWS MCP (awslabs.aws-api-mcp-server)
  4. Playwright MCP (@playwright/mcp)

MCP configuration is NOT a factor.

Related Issues

  • #10493 - Documents similar epoll_pwait(timeout=0) busy-wait behavior in Claude 2.0.28

Impact

Severity: HIGH

  • 75% of sessions affected immediately at startup
  • 60-70% CPU waste per affected session
  • No workaround available - bug is in compiled binary
  • Indefinite duration - does not self-resolve
  • Production systems impacted - multiple concurrent sessions multiply CPU waste
  • 4 concurrent sessions = 200%+ CPU waste (should be ~2-3% total)

Workarounds

None available.

  • Cannot disable at runtime
  • Killing MCP servers has no effect
  • Downgrading to v2.1.20 unlikely to help (bug exists across versions per #10493)
  • Only solution: Kill affected sessions and restart (gambling on 25% chance of healthy session)

Requested Fix

  1. Immediate: Identify why some sessions enter busy-wait while others don't (race condition at startup?)
  2. Root cause: Fix event loop to use blocking epoll_wait() with proper timeout (not zero)
  3. Long-term: Add CPU usage monitoring/alerting to detect busy-wait loops in production
  4. Release: Patch v2.1.26 with fix

Questions for Maintainers

  1. Why does session 1623341 sleep normally on ep_poll while others spin with WCHAN = -?
  2. What changed between session startup that causes 75% to enter busy-wait?
  3. Is the JITWorker thread (present in broken sessions, absent in normal) related?
  4. Can you add runtime detection of timeout=0 in epoll calls to log when this occurs?

Diagnostic Commands Used

# CPU usage
ps aux | grep 'claude --resume'

# Thread-level analysis
ps -L -p <PID> -o pid,lwp,nlwp,%cpu,wchan:20,comm

# Process state
ps -p <PID> -o pid,ppid,nice,pri,psr,stat,wchan:20

# Context switches
cat /proc/<PID>/status | grep -E '^(Threads|voluntary|nonvoluntary)'

# MCP servers
pgrep -P <PID>

Reporter Information

  • User: Michael Jabara (ceo)
  • Date: 2026-01-30
  • Investigation Duration: 90+ minutes across 4 sessions
  • Analysis Assistance: Codex MCP session 019c0df5-6ba4-7c92-ad8e-9b3bcf943048

---

This bug renders Claude Code unusable in multi-session production environments. Urgent fix required.

View original on GitHub ↗

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