[Bug] Event-loop starvation causing ~200% CPU spin and input lag after hibernate/resume

Status Open
Maintainer reply None cached
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 in claude!uv_run → KERNELBASE!GetQueuedCompletionStatusEx → ntdll!NtRemoveIoCompletionEx. i.e. the IOCP poll returns essentially instantly and the loop re-spins — the Windows equivalent of the uv_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 socket recv/send. Only one idle mswsock IOCP 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

  1. Start a Claude Code session (TUI), let it go idle at the prompt.
  2. Hibernate the laptop; later resume from hibernate.
  3. Observe the pre-existing claude.exe pinned 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()==0 hypothesis from #62308 fits the Windows stack (non-blocking GetQueuedCompletionStatusEx). 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 + cdb stack 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._debugProcessOpenFileMappingW 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.

View original on GitHub ↗

4 Comments

zippypaul · 1 month ago

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

  • Claude Code v2.1.218, Windows 11 Pro (build 26200)
  • Terminal: WezTerm + PowerShell, not Windows Terminal
  • Mixed MCP setup: several local stdio child servers (Playwright, gitlab-mcp, pytaiga-mcp, etc.) plus remote ones

Observed

  • Session was started ~21h before the freeze, alive across an OS sleep/resume in between.
  • Idle at the prompt (post /clear, no turn running) but claude.exe sustained ~200%+ CPU (measured directly: ~8.3 CPU-seconds accrued per 4 wall-clock seconds, sampled twice to confirm it was continuous, not a burst).
  • A second, independent claude.exe/WezTerm pair on the same machine, started fresh, was completely unaffected — confirms this is per-process, not host-wide contention.
  • Checked all 10 child processes of the stuck 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-NetTCPConnection for 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).
  • Didn't have ProcDump/WinDbg staged this time, so no fresh stack sample to add — but now have Process Monitor and will grab ProcDump ahead of time to capture the exact stack next occurrence, since this is a recurring issue on this machine (second time in recent memory).
  • Restarting the stuck session was the only workaround needed, matching your report.

Happy to share a fresh procdump capture next time this reproduces if it'd help triage.

zippypaul · 1 month ago

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 /availablesleepstates shows S3 disabled in favor of Modern Standby on this machine. The Microsoft-Windows-Power-Troubleshooter log gives an exact window:

  • Sleep Time: 2026-07-22 22:58:35 (local, UTC+8)
  • Wake Time: 2026-07-23 09:06:43 (local) — ended by lid open
  • ~10h08m continuous low-power period, confirmed by paired Kernel-Power 506/507 "entering/exiting Modern Standby" events right at those timestamps.

New finding — orphaned sockets in Bound state: ~21h after the stuck process started (and ~11h after the resume), Get-NetTCPConnection for the PID shows:

  • 45 TCP sockets stuck in Bound state — never reached Established, RemoteAddress 0.0.0.0, RemotePort 0.
  • Ephemeral port range consumed: 49394–65190.
  • Rechecked 15s later: count unchanged (45, no growth) — so this isn't an active per-tick leak happening right now; it reads more like a burst of failed/abandoned connection attempts (plausibly around the resume) that were never closed, sitting alongside the ongoing event-loop spin.

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 the uv_backend_timeout()==0 hypothesis, 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).

zippypaul · 1 month ago

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:

  • All net-class PnP devices report Status: OK — no ghost/error adapters.
  • Only one clean default route (IPv4, single interface, no ambiguous multi-homing).
  • No IPv6 default route at all on this machine (link-local + non-routable ULA only) — so IPv4/IPv6 Happy-Eyeballs racing isn't in play.
  • Teredo: disabled/offline. 6to4: inactive. Neither tunneling protocol is doing anything.
  • Tailscale (an actively-used VPN overlay on this box) is healthy and not configured as an exit node / default route — it isn't swallowing general traffic.
  • Several extra idle Wi-Fi virtual adapter instances are just how this dual-band-simultaneous chipset enumerates itself, not leftover cruft.

So the stuck Bound sockets 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.

zippypaul · 1 month ago

Correction on version: I stated v2.1.218 above, but that was read from a live FileVersionInfo check 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 fresh claude --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.