[Bug] Event-loop starvation causing ~200% CPU spin and input lag after hibernate/resume
Status Open
Maintainer reply None cached
Workaround ✓ Mentioned in thread ↓
Activity 4 comments · opened Jul 23, 2026
[BUG][Windows] Idle session spins ~2 cores after hibernate/resume — libuv event loop stuck with uv_backend_timeout() == 0 (Windows analog of #62308); self-terminates after ~tens of minutes
Likely the same underlying loop bug as #62308 (closed stale, macOS) — this is a Windows reproduction with a different trigger context. Related idle-CPU reports: #69251 (Windows, Ink render heartbeat — different mechanism), #19393 / #22275 (Linux idle 100% CPU).
Environment
- Claude Code v2.1.217 (bundled-Node single-file install)
- Windows 11 Pro 26200, Windows Terminal + PowerShell 7, 24 logical cores
- A handful of remote HTTP/SSE MCP servers attached (all remote — no stdio MCP child processes)
Summary
A session that is already running when the laptop resumes from hibernate begins burning ~184–200% CPU (≈2 cores) while idle at the prompt (no model turn running, user not typing). It starves the Ink input handler, so keystrokes take several seconds to render. A session started after the resume is unaffected. The spin is self-terminating — it runs for tens of minutes (observed 40+ min) and then drops back to idle on its own; restarting the session is the instant fix.
What the native stacks show (procdump + WinDbg cdb, MS public symbols)
!runaway: the main V8 event-loop thread dominates (~28 min user CPU), plus ~7 V8 background/helper threads (~3.5–5.3 min each).- Sampling the main thread across 8 dumps (~14 s): 7/8 OFF the libuv poll — in V8 JIT'd JavaScript or
ntdll!RtlReleaseSRWLockExclusive(a V8 heap lock); only 1/8 inclaude!uv_run → KERNELBASE!GetQueuedCompletionStatusEx → ntdll!NtRemoveIoCompletionEx. i.e. the IOCP poll returns essentially instantly and the loop re-spins — the Windows equivalent of theuv_backend_timeout()==0/ zero-timeout poll described in #62308. - Helper threads: always parked on a condition variable, accruing CPU in short bursts (V8 background workers/GC).
What it is NOT (ruled out from 8 samples × ~40 threads = 320 observations)
- Zero DNS (
getaddrinfo/dnsapi), zero zlib/compression, zero TLS/crypto (ncrypt/bcrypt/schannel), zero socketrecv/send. Only one idlemswsockIOCP helper. - Sockets are stable: a few established :443 connections, 0 SYN_SENT, 0 TIME_WAIT, no churn. So it is not a native TLS/socket/zlib read loop, not DNS/reconnect churn, not connection thrashing, and not an MCP subprocess (all MCP servers are remote HTTP; no child processes). It's a pure in-process event-loop spin.
Timeline correlation
- Onset coincides with the OS resume (Kernel-Power event 507/107). CPU accounting pins it: total process CPU (~64 min over a 4h06m life) at the observed ~1.9 cores needs ≥31 min to accrue ⇒ onset ≈ the resume (~3% CPU before, ~190% after).
- Reproduced across two independent sessions live through the same resume; both spun and both self-recovered independently.
Steps to reproduce
- Start a Claude Code session (TUI), let it go idle at the prompt.
- Hibernate the laptop; later resume from hibernate.
- Observe the pre-existing
claude.exepinned at ~2 cores while idle; input lag in that session. A session started after resume is fine. It clears itself after tens of minutes.
Notes for triage
- The
uv_backend_timeout()==0hypothesis from #62308 fits the Windows stack (non-blockingGetQueuedCompletionStatusEx). A likely trigger is a socket/handle that hibernate left in a state that keeps the loop's computed poll timeout at 0 (dead/half-open connection or a timer at t=0). - Full user-mode memory dumps +
cdbstack logs were captured and can be shared privately with Anthropic on request (not attached here — they contain process memory / credentials). - The shipped build has the Node inspector/debug-signal handler disabled (
process._debugProcess→OpenFileMappingW errno 2), so a running session can't be JS-profiled; naming the exact JS callsite needs an inspector-enabled build + hibernate repro, or symbolizing the dumps against the matching Node build.
4 Comments
Confirming an independent reproduction of this on v2.1.218, different terminal emulator (WezTerm, not Windows Terminal) and a sleep (not hibernate) trigger — so the trigger surface is broader than hibernate-only, and it's not specific to any one terminal frontend.
Environment
Observed
/clear, no turn running) butclaude.exesustained ~200%+ CPU (measured directly: ~8.3 CPU-seconds accrued per 4 wall-clock seconds, sampled twice to confirm it was continuous, not a burst).claude.exe/WezTerm pair on the same machine, started fresh, was completely unaffected — confirms this is per-process, not host-wide contention.claude.exe(the local MCP stdio servers): every one showed 0 CPU delta over the same sampling window — the spin is entirely in the parent process, consistent with this being an in-process event-loop issue rather than an MCP subprocess problem.Get-NetTCPConnectionfor the stuck PID showed zero established connections at sample time (may just be a snapshot artifact given your own finding of stable sockets — noting it in case it's a useful additional data point rather than a contradiction).Happy to share a fresh procdump capture next time this reproduces if it'd help triage.
Follow-up with more precise data — deliberately kept the stuck session alive rather than restarting it, to gather more evidence.
Trigger verified via Windows Event Log (not assumption): this was Modern Standby (S0 Low Power Idle), not classical Hibernate.
powercfg /availablesleepstatesshows S3 disabled in favor of Modern Standby on this machine. TheMicrosoft-Windows-Power-Troubleshooterlog gives an exact window:Kernel-Power506/507 "entering/exiting Modern Standby" events right at those timestamps.New finding — orphaned sockets in
Boundstate: ~21h after the stuck process started (and ~11h after the resume),Get-NetTCPConnectionfor the PID shows:Boundstate — never reachedEstablished,RemoteAddress 0.0.0.0,RemotePort 0.This is a different shape than the original report's "sockets are stable... 0 SYN_SENT, 0 TIME_WAIT, no churn" — worth checking whether a
Bound-but-never-Established/never-closed socket handle could itself be the dangling handle that keeps the computed poll timeout pinned at zero in theuv_backend_timeout()==0hypothesis, rather than (or in addition to) a pure in-process V8/GC spin.Duration differs too: this instance has been spinning continuously for 21+ hours (process start 2026-07-22 13:29:59, still at ~200% CPU as of this comment) — well beyond the "self-recovers after tens of minutes (observed 40+ min)" behavior in the original report. Kept it alive intentionally in case anyone wants a live target to inspect (e.g. via a shared procdump) before I restart it.
Environment unchanged from previous comment (v2.1.218, WezTerm on Windows 11 Pro 26200).
Ruled out local network misconfiguration as an explanation for the 45 orphaned
Bound-state sockets mentioned above, in case it's a useful negative result:Status: OK— no ghost/error adapters.So the stuck
Boundsockets don't look like a routing/adapter artifact specific to this machine — more consistent with being in-flight connection attempts caught at the exact moment the event loop started spinning, then left permanently incomplete because the loop never services their completion afterward.Correction on version: I stated v2.1.218 above, but that was read from a live
FileVersionInfocheck against the still-running PID's binary path just now, which only reflects what's currently on disk — not necessarily what was loaded into memory when the process actually started (2026-07-22 13:29:59, ~21h+ before this correction). The CLI auto-updates its on-disk binary in place without killing running sessions, so a long-lived process can be executing older code while a freshclaude --version/file check reports the newer installed version. Per my own record, that session started on v2.1.216, not v2.1.218. This also means that an update must have happened between the time we started it and now when we see this issue.