[Bug] Idle session busy-spins CPU with sched_yield in worker threads instead of blocking
Bug Description
Idle Claude Code session busy-spins CPU instead of sleeping. When a session is idle at the prompt (last turn stop_reason=end_turn, waiting for input), it burns ~3-9% of a CPU core continuously and never quiesces. It happens whether or not a terminal is attached, so it's not an attached-TUI redraw artifact.
strace on an idle session (2s sample, ~4% CPU, ~27 threads) is dominated by sched_yield + futex, not epoll:
50.03% futex 381 calls (67 errors) 24.47% restart_syscall 12 calls 22.16% sched_yield 604 calls 2.34% epoll_pwait2 8 calls
sched_yield firing 600+ times in 2s on an idle process = a busy-yield spin. The main thread is correctly parked in epoll_pwait2; the ~27 worker/render threads spin. Thread count is stable (not a leak).
Impact: fine for one session, but we keep many sessions persistent (so they can be resumed from different editor instances). ~30 idle sessions synchronized their spin and drove a 16-core host to load average 114 until we killed the idle ones.
Env: Claude Code 2.1.207, Node v22.22.0, Ubuntu 24.04 (kernel 6.8), 16 cores. Sessions run headless under dtach (claude --dangerously-skip-permissions --session-id ); reproduces with an attached terminal too.
Repro: start a session, let it go idle at the prompt, leave it, then strace -f -c -p <pid> for 2s -> dominated by futex+sched_yield, not epoll. CPU sits at a few percent, not 0%.
Expected: an idle session should be parked (blocked in epoll/read) at ~0% CPU until input/event arrives.
Likely area: a render/animation loop or worker-thread condition-wait that falls back to sched_yield and doesn't quiesce when there's nothing to render and no input pending (Ink loop / interval timer / thread-pool wait). Main thread is fine; the worker threads are the ones spinning.
Environment Info
- Platform: linux
- Terminal: vscode
- Version: 2.1.207
- Feedback ID: 8c8c04e0-e808-4bcb-88c8-7f551eae0348
Errors
[]
3 Comments
Thanks for the detailed report and the strace methodology — that made this easy to check.
I ran your exact repro on 2.1.233 (Linux x64, tmux, fresh directory): started a session, completed one prompt/reply turn ending in
end_turn, left it idle at the prompt, and measured.top -b -d 5, plus utime/stime deltas)strace -f -cfor 5 s in both states: zerosched_yieldcalls — the process sits in blockingfutex/epoll_pwait2waits (~100–210 syscalls total in 5 s, vs your 600+ yields in 2 s)So the busy-yield spin you saw on ~2.1.207 no longer reproduces. The changelog for 2.1.233 includes a fix for idle sessions on Linux keeping a CPU core busy, which matches this class of issue.
Closing as fixed in 2.1.233 — please update (
claude update) and reply with fresh%CPU/strace numbers and yourclaude --versionto reopen if you still see it.🤖 Generated with Claude Code
Thanks for running it — and the zero-
sched_yieldresult is reproducible here too, which turns outto be the useful part. I still see the spin on 2.1.233, and I think the difference is heap size,
not version.
claude --version→2.1.233 (Claude Code), native build (ELF, Bun 1.4.0 / JavaScriptCore) at~/.local/share/claude/versions/2.1.233. Ubuntu 24.04.4, kernel 6.8.0-137, 16 cores. Sessions runheadless under
dtachwith--dangerously-skip-permissions, so sandboxing is off.Fresh numbers on 2.1.233
Five real sessions, idle at the prompt 15–55 minutes, per-thread CPU from
/proc/<pid>/task/*/statdeltas over a 120 s window:
| pid | idle CPU (% of one core) |
|---|---|
| 849629 | 2.04% |
| 916709 | 1.74% |
| 1443619 | 1.56% |
| 1480297 | 1.50% |
| 1483749 | 1.50% |
| total | 8.34% |
strace -f -c -p <pid>for ~6 s, three of those sessions, all idle:So: better than the 3–9% I originally reported on 2.1.207, but not parked.
Where the yields come from
strace -f -e trace=sched_yield, 5 s on pid 849629, attributed per thread:HeapHelperis JSC's parallel GC marker pool —pool->ensureThreads(Options::numberOfGCMarkers() - 1)inheap/HeapHelperPool.cpp, sized from theprocessor count. 16 cores here → 8 markers → exactly the 7 helper threads above. On a smaller box
there are fewer helpers and proportionally less spin.
Splitting the 8.34% by thread class:
Why a fresh-directory repro shows zero
I think this is the whole discrepancy. Idle cost tracks heap size, not version or idle time:
| session | RSS | HeapHelper threads | idle CPU |
|---|---|---|---|
| fresh, never prompted, no MCP servers at all | 249 MB | 7 | 0.17% |
| fresh, never prompted, full MCP set | 249 MB | 7 | 0.18% |
| resumed 2.4 MB transcript | 472 MB | 7 | 0.55% |
| real working sessions | 528–691 MB | 7 | 1.50–2.04% |
A fresh session has a small heap, so each GC is trivial and the helpers barely spin — which is
exactly your ~0.1% / ~0.8% result, and I reproduce those numbers here. A session carrying hours of
conversation and a multi-MB transcript has a ~600 MB heap, and every GC wakes 7 helpers that
spin-yield through the marking handshake.
Cheapest way to reproduce without spending any tokens — clone a large transcript and resume it:
(The pool is created lazily and a bit non-deterministically — I saw it appear 5 s after start in one
session and still absent at 90 s in two others. A 60 s window on a just-started session can miss it
entirely.)
Isolating it
Same cloned transcript resumed twice, once with
BUN_JSC_numberOfGCMarkers=1, both idle,strace -f -cfor ~6 s:sched_yieldgoes to zero, total syscalls drop ~9×, idle CPU 0.55% → 0.38%. So the yields are theJSC marker pool's spin-wait, not anything in the Claude Code event loop — the main thread stays
correctly parked in
epoll_pwait2throughout.Two notes on the close
when sandboxing is enabled"*. That is a different fault from this one: sandboxing is off in
every session here, and the shape is a few percent per session multiplied by many sessions, not
one core pegged.
spin) — still present in 2.1.220") is still open with the same syscall fingerprint at a much
larger magnitude.
Happy to run anything else you'd like measured. If a marker-pool cap is not desirable by default,
even documenting
BUN_JSC_numberOfGCMarkersas a supported knob for many-session hosts would helpus — we keep ~30 sessions resident so they can be resumed from different editor instances, and the
per-session cost is what multiplies into a real number.
🤖 Prepared with Claude Code
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.