Idle CLI sessions burn 100%+ CPU each in recurring ~1.1h episodes (~11 CPU-hours per episode); originally attributed to sleep/resume — refuted by instrumentation

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 2 comments · opened Jul 26, 2026
## ⚠️ UPDATE 2026-07-27 — caught live and instrumented; three claims below are wrong Full data: instrumented follow-up comment. The original text is kept unedited for history, with inline **[CORRECTED …]** notes at the affected claims. Corrections in brief: 1. Suspends here are 9–18 seconds, not ~4 hours. Every Kernel-Power 42 → 107 pair measured is seconds long; the ~4 h is the interval between suspends. I misread my own evidence. This invalidates the "~144,000 catch-up iterations" arithmetic in Suspected cause. 2. The burn does not start at resume. Proved by a CPU budget: a session at 204% since the last resume would need ~28,600 s of CPU but had used 2,807 s in its entire life. Onset was ~3.5 h after the resume, with no OS event at the time. 3. Recency of activity is inverted. The worst offender (202% steady) had been idle 7 days. Also new: disk I/O measured and negligible (pure CPU spin — the state-rewrite-loop hypothesis is excluded); the daemon and pty-host processes are affected too, not just interactive sessions; Get-Process claude* conflates Claude Desktop with Claude Code (10 of 22 processes here were Desktop Electron children, all at 0%); and one episode cost ≈11 CPU-hours across 7 processes, each running ~1.1–1.2 h at ~200% before stopping by itself. The suggested fix at the bottom (clamping timer deltas on resume) would not address this.

Environment

  • Claude Code: 2.1.220 (npm install; claude.exe is a 253 MB self-contained binary)
  • CLI runtime: Bun / JavaScriptCore (binary contains bun-windows-x64, BunError,

__bun_, JSGlobalObject; the libuv / v8::internal strings also present are
Bun's Node-compat and N-API/V8-ABI shims, not an actual V8 or libuv loop)

  • Node on PATH: v22.13.1 — used only by MCP subprocesses, not by the CLI
  • OS: Windows 11 Pro 10.0.26200
  • CPU: AMD Ryzen 9 5950X (16C/32T)
  • Terminal: Windows Terminal

Summary

Primary, measured claim. Claude Code sessions that are open when the machine enters
sleep come back from resume burning 100%+ CPU each for a long time — well over an hour
in my case — before returning to normal on their own. Three sessions on this machine
were burning ~5 cores between them while sitting idle at a prompt, with no task running.
The condition is self-limiting: all three returned to near-idle without any
intervention (no kill, no restart). So this is a bounded catch-up storm rather than an
unbounded spin — but it burns multiple cores for hours on every resume.

Correlated observation, mapping unconfirmed. Separately, one session's UI became
nearly unusable over the same period: cursor movement via arrow keys and typing both
lagged by 10–30 seconds per keystroke, and it later returned to normal by itself. This
is consistent with the saturated thread being the one that renders the Ink TUI and reads
stdin — the terminal is starved rather than at fault, which is why it presents as a
terminal emulator problem.

I could not confirm that the frozen window was one of the three high-CPU processes.
The identification was attempted with echo $$ from the Bash tool, which on Windows
returns an ephemeral MSYS2 emulated pid from a per-session namespace — not a Windows
pid, and dead within seconds — so the mapping is unrecoverable for this occurrence.
Treat the CPU measurements and the freeze as correlated but not proven to be the same
process.

Reproduction

Not reliably reproducible on demand, but the correlation is consistent:

  1. Open a session and run a task through to completion.
  2. Leave it idle at the prompt — do not close it.
  3. Let the machine idle-sleep some hours later. Mine went through two suspends of

roughly four hours each, confirmed via Kernel-Power event IDs 42 (sleep) and
107 (resume).
**[CORRECTED 2026-07-27 — this is a misreading. Every 42 → 107 pair measured is
9–18 seconds long; ~4 h is the spacing between suspends, not their duration.
See the follow-up comment.]**

  1. Resume. The session is now pegged at 100%+ CPU while still idle, and input latency

climbs to 10–30 seconds per keystroke.

  1. Leave it alone. It returns to normal by itself, but only after a long grind — mine

was still above 200% roughly 1h45m after resume, and had dropped to ~12% about two
hours after that.

Surviving sleep is necessary but not sufficient. Session age is not the variable either
— the longest-running session of all was unaffected. Everything below about the state
at sleep is reconstructed from memory, not instrumented
, and is offered only as a
possible lead:

  • To the best of my recollection the three affected sessions were not mid-task when

the machine suspended. I had left the PC while they were working, and they had most
likely finished and gone idle by the time it slept about an hour later. I cannot
confirm this.

  • If that is right, the distinguishing variable is recency of activity rather than

in-flight work: the three affected sessions had each completed work within roughly an
hour before the suspend, whereas the healthy session (~142h uptime, ~10 resume cycles,
1.6% CPU) had been idle for days.
[CORRECTED 2026-07-27 — inverted. The worst offender measured (202% steady) had been
idle for 7 days and had burned 12.5 CPU-hours doing nothing. Recency does not predict it.]

  • That would point at a long-period timer armed by activity being pending across the

suspend. A fast UI or spinner tick is a poorer fit, since every session would hold one
equally.

Attributing each session to a specific suspend event is also approximate.

Evidence

Instantaneous CPU, sampled as a delta over 6s (cumulative CPU is misleading — it
just reflects how much work a session did):

| Session | Uptime at sample | % of one core | Resumes survived |
|---------|------------------|---------------|------------------|
| A | ~8h | 203% | 2 |
| B | ~42h | 202% | ~6 |
| C | ~12h | 106% | 2 |
| D | ~142h | 1.6% (fine) | ~10 |

Sessions A, B and C are the affected ones; D is the healthy control.

Per-thread breakdown shows a single pegged thread per process (81% / 100% / 61%),
consistent with the main event-loop thread. Several secondary threads ran at 7–37% and
account for the excess over 100%; I did not identify what those threads are (GC and JIT
would be the obvious candidates under JSC, but I did not verify that).

No network I/O is involved:

  • Get-NetTCPConnection shows 0 SynSent across repeated samples — no connection

attempts, so this is not a reconnect/retry storm.

  • Established connections: 0, 0, and 1 for the three affected processes, versus 8

for a healthy session started after resume.

  • Handle count flat across samples (427 / 337 / 427) — not a handle or fd leak.
  • Thread count flat at 51 — not thread churn.

Not measured — disk I/O. I did not sample per-process disk throughput, so a loop
that repeatedly rewrites session state on disk is not excluded by the above; it would
present with exactly these flat handle and socket counts. Anyone reproducing this should
check Get-Counter "\Process(claude*)\IO Data Bytes/sec" (or equivalent) to separate a
pure CPU spin from a disk-write loop. My processes had already recovered before I thought
to check.

Re-sampled the same processes some hours later, with no intervention of any kind:
session A had fallen from 203% to 12%, and B and C were below 3%. The work is therefore
finite — it completes — but takes hours of full-core CPU to do so.

Suspected cause

The runtime is Bun/JSC (confirmed above), so this is Bun's timer implementation rather
than libuv. Timers are scheduled against a monotonic clock, so after a multi-hour
suspend every pending timer is hours overdue.

The fact that it eventually resolves on its own points specifically at a bounded
catch-up loop
rather than a runaway re-arm: something like
while (next <= now) { next += interval; tick() }, iterating once per missed period to
close the gap.
[CORRECTED 2026-07-27 — the premise is false: suspends here are 9–18 s, so a 100 ms
interval gives ~120–180 iterations, not ~144,000. The burn is also not proportional to
suspend duration, and does not begin at resume at all.]

A 4-hour suspend against a 100ms interval is ~144,000 iterations; if
each iteration does real work (a re-render, a transcript pass, a state write) that
grinds for a very long time and then terminates — which matches the observed behaviour
exactly, including why it recovers unaided.

An unbounded setTimeout(fn, deadline - now) with a negative delay clamping to 0 would
also produce the CPU signature, but it would never recover, so the evidence argues
against it.

Given that the affected sessions had been active within about an hour of the suspend
while the unaffected one had been idle for days, the candidate is a long-period timer
armed by activity — a credential/token refresh, a rate-limit window reset, a session
persistence or compaction tick, or a keepalive — rather than a fast UI/spinner tick,
which every session would hold equally.

Impact

  • Multiple cores burned for hours after every resume; on a laptop this would be a

battery and thermal problem.

  • At least one session was effectively unusable while it lasted, at 10–30s input

latency (see the mapping caveat in the Summary). Because this reads as a terminal
emulator bug rather than a Claude Code bug, users are likely to misattribute it and
never report it — which may explain why the related issues below are numerous,
vague, and consistently closed without diagnosis.

  • **[UPDATED 2026-07-27 — quantified: one episode ran ~1.1–1.2 h at ~200% per process and

cost ≈11 CPU-hours across 7 processes; it then stopped unaided. It recurs, so the same
process pays this repeatedly over days.]**

  • It does self-resolve, so killing the process is not strictly required; but the wait

is long enough that killing and relaunching with --resume is the practical
workaround.

Related issues

The "high CPU while idle" symptom has been reported repeatedly. I opened and verified
the status of these five — all are closed, none with an identified root cause, and none
isolates the sleep/resume trigger:

  • #32633 — Windows sleep/wake with active session (segfault, not spin) — closed as duplicate
  • #21006 — spin loop after idle / terminal switch — closed as duplicate
  • #22256 — CPU spinning when idle or blocked on I/O — closed as not planned, stale
  • #30807 — high CPU when idle, 10-90% per instance — closed as not planned, stale
  • #44040 — idle sessions consume ~5% CPU each — closed as not planned

These additionally appear to describe the same symptom, but I have not opened them and
cannot vouch for their status or content: #19393, #22275, #22509, #17148, #8188.

Worth noting that #32633 is also a Windows suspend/resume failure under the Bun runtime,
albeit a segfault rather than a CPU burn — two different symptoms, same runtime, same
trigger, which may point at a Bun-level suspend/resume gap rather than application logic.

What this report adds over the above: a specific trigger (OS suspend/resume), a control
case (a session that survived ~10 resumes while staying healthy), the self-limiting
behaviour (which constrains the mechanism to a bounded loop), and negative evidence
excluding a reconnect storm, a handle leak and thread churn.

Suggested fix

[SUPERSEDED 2026-07-27 — the fix below addresses a resume-time clock jump, which the
measurements rule out as the trigger. The live lead is instead a multi-file rewrite of
dormant ~/.claude/projects/*.jsonl transcripts that twice immediately preceded a burn,
on return-to-active rather than at S3 resume. See the follow-up comment.]

Clamp timer deltas to a sane minimum on re-arm, and detect large monotonic-clock
jumps on resume to collapse overdue catch-up iterations into a single fire.

View original on GitHub ↗

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