[Bug] High CPU usage at idle/prompt state

Status Open
Reported on v2.1.29
Maintainer reply None cached
Activity 12 comments · opened Feb 2, 2026

Bug Description
Claude code 2.1.29 uses close to 100% of my CPU while doing nothing (I'm at prompt). Actually, I've just started a new claude haven't executed anything yet and the CPU usage is 20+ percent.

Environment Info

  • Platform: darwin
  • Terminal: Apple_Terminal
  • Version: 2.1.29
  • Feedback ID: 4044b938-d75f-4830-ae82-cfa2e3a4500b

Errors

[{"error":"Error: LSP server plugin:rust-analyzer-lsp:rust-analyzer crashed with exit code 1\n    at <anonymous> (/$bunfs/root/claude:2142:21472)\n    at emit (node:events:98:22)\n    at #handleOnExit (node:child_process:520:14)\n    at processTicksAndRejections (native:7:39)","timestamp":"2026-02-02T06:45:39.770Z"}]

View original on GitHub ↗

12 Comments

github-actions[bot] · 6 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/22253
  2. https://github.com/anthropics/claude-code/issues/17148
  3. https://github.com/anthropics/claude-code/issues/22256

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

arslanbekov · 6 months ago

Same problem for me:

> claude --version
2.1.29 (Claude Code)

Platform: darwin
Terminal: ghostty

98.4 CPU Usage AVG

banagale · 6 months ago

I had three idle processes using ~100%, draining my battery for a day.

I could actually see a few long running idle instances of .23 alongside them not making any such fuss.

I downgraded to .25 to deal with the (now solved) resume bug in .27 and the CPU problem went away.

<img width="320" height="155" alt="Image" src="https://github.com/user-attachments/assets/1a844bb3-fa71-49eb-b368-73be2eb02344" />

ulassel · 6 months ago

I was having the same issue then I found out there are 134 orphan subagents, after killing those my cpu usage decreased to normal levels.

Quelaan1 · 6 months ago

Same issue

jeffgicharu · 6 months ago

Adding detailed Linux diagnostic data with root cause analysis:

Platform: Linux (Kali 6.12.13-amd64)
Version: 2.1.29
Terminal: External terminal

---

strace analysis (3 second sample of idle process)

| Syscall | Calls | % Time | Purpose |
|---------|-------|--------|---------|
| madvise | 6,456 | 11% | GC memory hints |
| sched_yield | 301 | 5% | CPU yielding |
| futex | 1,499 | 11% | Thread sync |
| epoll_pwait2 | 145 | 71% | Event waiting |

All madvise calls are MADV_DODUMP (~2,152/sec) — Bun's garbage collector is stuck in a tight loop.

Memory at time of spin

  • RSS: 1.78 GB
  • VmData: 65 GB (severe heap fragmentation)
  • Session age: ~2 hours of active use, then idle

Key finding

Disabling rust-analyzer-lsp plugin did NOT fix the issue. The madvise/GC loop persists even with the plugin disabled. This rules out the plugin crash as the root cause.

Root cause

After sessions accumulate memory during active work, heap fragmentation causes the Bun GC to run continuously without ever settling into idle state. The GC keeps marking/unmarking memory pages even when there's no actual work to do.

Workaround

Kill long-running idle sessions. The bug doesn't appear immediately on fresh sessions — it develops after significant memory accumulation during active use.

terrylica · 6 months ago

macOS Diagnostic Data: Idle Sessions at 80-100% CPU

Environment

  • Claude Code Version: 2.1.27, 2.1.29, 2.1.30
  • macOS: 15.7.4 (24G508)
  • Hardware: MacBook Pro M3 Max, 14 cores, 36GB RAM
  • Terminal: iTerm2

Observed Behavior

10 concurrent Claude Code sessions accumulated over days, all with --continue flag for session persistence. Despite being completely idle (waiting for user input), they consumed 667% total CPU.

| Session | Version | Elapsed | CPU% | Memory |
|---------|---------|---------|------|--------|
| A | 2.1.29 | 2d 12h | 95% | 587MB |
| B | 2.1.27 | 3d 22h | 92% | 745MB |
| C | 2.1.29 | 1d 20h | 92% | 530MB |
| D | 2.1.29 | 7h 40m | 78% | 544MB |
| E | 2.1.27 | 3d 11h | 54% | 814MB |
| F | 2.1.29 | 13h | 49% | 457MB |
| G | 2.1.27 | 3d 22h | 14% | 611MB |
| H | 2.1.29 | 1d 20h | 98% | 2.0GB |

System impact: Load average 10.23, swap 9.4GB/10GB used, 24% idle CPU.

Key Finding: /clear Command Fixes the Issue

After running /clear in session H:

| Metric | Before /clear | After /clear |
|--------|-----------------|----------------|
| CPU | 98% | 0.0% |
| Memory | 2.0GB | 160MB |

This confirms the issue is related to accumulated session state causing continuous CPU consumption, not the idle input loop itself.

Thread Analysis (via ps -M)

Main thread shows state R (running) at 78-87% CPU even when idle. Worker threads (3-4 per process) show ~1% each. Main thread is doing work, not blocked waiting for input.

sample Output Analysis

3-second sample shows:

  • 2430 samples on main thread
  • All samples in <unknown binary> addresses (JIT-compiled code)
  • Consistent with GC thrashing pattern reported by others

Correlation with Session Age and Memory

| Session Age | Memory | CPU |
|-------------|--------|-----|
| < 8h | < 550MB | 50-80% |
| 1-2d | 500-600MB | 90%+ |
| 3-4d | 600-800MB | 85-95% |

Older sessions with more accumulated state run hotter, but even newer sessions (7h) show 78% CPU when idle.

Workaround Confirmed

Running /clear immediately drops CPU to 0% and reduces memory significantly. This suggests the fix should target whatever state accumulation triggers the GC thrashing.

Questions for Developers

  1. What data structures grow unbounded during a session that /clear resets?
  2. Is there a way to trigger periodic cleanup without losing context?
  3. Could a memory threshold trigger automatic compaction?

---

Related findings from other users in this thread: GC thrashing (mmap/munmap cycles), Bun heap fragmentation after memory accumulation.

todd-dsm · 6 months ago

I'm seeing the same behavior on macOS:

  • Claude became very laggy
  • The macOS system was bogging and
  • Typing (in Terminal.app) was pretty sluggish
  • After /compact the system became way less laggy but

This is excessive for idling:

% ps -M -p "$PID" -o pid,%cpu,comm
USER     PID   TT   %CPU STAT PRI     STIME     UTIME COMMAND   PID  %CPU COMM
$user 35789 s007   80.6 R    31T   8:38.41 487:20.24 claude  35789  80.6 claude
       35789         0.0 S    31T   0:00.37   0:00.15         35789   0.0 
       35789         0.0 S    31T   0:00.91   0:00.15         35789   0.0 
       35789         0.0 S    31T   0:12.14   0:05.75         35789   0.0 
       35789         0.0 S    31T   0:00.96   0:00.15         35789   0.0 
       35789         0.0 S    31T   0:00.47   0:00.15         35789   0.0 
       35789         0.0 S    31T   0:00.45   0:00.15         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:02.29   0:00.99         35789   0.0 
       35789         0.0 S    31T   0:00.73   0:00.16         35789   0.0 
       35789         0.0 S    31T   0:00.39   0:00.16         35789   0.0 
       35789         0.0 S    31T   0:01.27   0:00.15         35789   0.0 
       35789         0.0 S    31T   0:01.85   0:00.16         35789   0.0 
       35789         0.0 S    31T   0:01.41   0:00.15         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:00.16   0:04.05         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 
       35789         0.7 S    31T   0:38.24   0:17.72         35789   0.7 
       35789         0.3 S    31T   0:08.63   1:23.38         35789   0.3 
       35789         0.4 S    31T   0:08.13   1:24.47         35789   0.4 
       35789         0.4 S    31T   0:08.59   1:19.41         35789   0.4 
       35789         0.0 S    31T   0:00.42   0:19.05         35789   0.0 
       35789         0.0 S    31T   0:00.00   0:00.00         35789   0.0 

Also, I don't recall seeing this before v2.1.29; seems new.

Caleb-KS · 6 months ago

Happens on Windows 11 too. Fairly recent, within a week.

sqdshguy · 6 months ago

Disclaimer: this comment was written by Claude Opus 4.6 (fast) and reviewed by me for AI slop. Take it with a grain of salt but definitely try the fixes - they literally made my M1 8GB/512GB Mac usable again.

Dug into this pretty deep on my M1 8GB, found 3 separate things causing the CPU/memory issues. Went from 208% CPU / 3.3GB RSS idle down to ~30% / 700MB.

I'm not a Bun/JSC internals expert, I just had my claude (Opus 4.6) trace through Bun source, fs_usage, vmmap etc until we figured out what's going on. So treat this as leads for the Anthropic team to investigate properly, not a definitive root cause report. That said, the workarounds do work.

1. Syntax highlighting is the biggest CPU hog

The syntax highlighter uses Oniguruma (via a Rust native module) with TextMate grammars - same regex engine as VSCode, and prone to the same backtracking issues on certain code blocks. highlight.js is only the JS fallback. Every render frame re-highlights everything visible so it just pins the CPU. I didn't even have code changes in the render view and it was still regexing away. Disabling it instantly dropped me from 208% to ~50%:

// ~/.claude/settings.json
{ "syntaxHighlightingDisabled": true }

2. Memory never gets returned to the OS

Two things here. mimalloc holds onto freed pages forever by default (saw 49% fragmentation in vmmap), and JSC's GC only triggers at 2x heap growth so gradual leaks slip through. This is what causes the 304GB vsize / multi-GB RSS people are seeing.

Workaround - make a wrapper script at ~/.local/bin/claude (or wherever it's first in your PATH):

#!/bin/bash
export MIMALLOC_PURGE_DELAY=0
export MIMALLOC_ARENA_EAGER_COMMIT=0
export BUN_JSC_gcMaxHeapSize=1500000000
export BUN_JSC_smallHeapRAMFraction=0.05
export BUN_JSC_mediumHeapRAMFraction=0.15
export BUN_GC_TIMER_INTERVAL=300

CLAUDE_BIN=$(ls -t "$HOME/.local/share/claude/versions/"* 2>/dev/null | head -1)
exec "$CLAUDE_BIN" "$@"

This got me from 3.3GB to ~700MB RSS and fragmentation from 49% to 3%.

3. Config file polling - 638 syscalls/sec while idle

This one's wild. Run sudo fs_usage -w -f filesys $(pgrep claude) | grep config.json and you'll see a wall of access() + stat64() on ~/.claude/.config.json at ~300+ pairs per second. It's being read every React render frame (the spinner drives the loop). The files never change. Still reproduces on 2.1.39 (44 hits in a single second burst during streaming).

Traced the root cause: there's a getConfig() function that does existsSync() (to resolve the path) + statSync() (for mtime-based caching) on every call - so even cache hits cost 2 syscalls. This function is called directly from React render paths (progress bar, spinner tree, verbose flag, tip checks, etc.) instead of going through the zustand store that already exists for settings. There are ~438 direct call sites bypassing the store. The store-backed path (useAppState(s => s.settings)) reads from memory and doesn't touch disk, but most components use the direct disk-reading function instead.

The fix on Anthropic's side would be to route those render-path reads through the existing zustand store, or add a time-based TTL to the stat cache instead of checking mtime every call. I built a DYLD interpose dylib that caches stat/access with a 200ms TTL, dropped it from 638/sec to 3/sec. If anyone wants to try it on macOS:

<details>
<summary>DYLD interpose dylib (macOS only)</summary>

Save as config_cache.c, compile and sign:

cc -O2 -dynamiclib -o ~/.local/lib/config_cache.dylib config_cache.c
codesign -s - ~/.local/lib/config_cache.dylib
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>
#include <mach/mach_time.h>

static mach_timebase_info_data_t tb;
static uint64_t ttl;

__attribute__((constructor))
static void init(void) {
    mach_timebase_info(&tb);
    ttl = (200000000ULL * tb.denom) / tb.numer;
}

static inline int is_hot(const char *p) {
    if (!p) return 0;
    size_t len = strlen(p);
    if (len < 12) return 0;
    const char *e = p + len - 12;
    return (memcmp(e, ".config.json", 12) == 0) ||
           (memcmp(e, ".claude.json", 12) == 0) ||
           (memcmp(e, "changelog.md", 12) == 0);
}

#define SLOTS 4
static struct { int r; int e; int v; uint64_t t; } ac[SLOTS];
static struct { int r; int e; int v; struct stat s; uint64_t t; } sc[SLOTS];

extern int access(const char *, int);
extern int stat(const char *, struct stat *);

int my_access(const char *path, int mode) {
    int h = is_hot(path);
    if (h) {
        uint64_t n = mach_absolute_time();
        if (ac[h].v && (n - ac[h].t) < ttl) {
            if (ac[h].r) errno = ac[h].e;
            return ac[h].r;
        }
        int r = access(path, mode);
        ac[h] = (typeof(ac[0])){r, errno, 1, n};
        return r;
    }
    return access(path, mode);
}

int my_stat(const char *path, struct stat *buf) {
    int h = is_hot(path);
    if (h) {
        uint64_t n = mach_absolute_time();
        if (sc[h].v && (n - sc[h].t) < ttl) {
            if (!sc[h].r && buf) *buf = sc[h].s;
            if (sc[h].r) errno = sc[h].e;
            return sc[h].r;
        }
        int r = stat(path, buf);
        sc[h].r = r; sc[h].e = errno; sc[h].v = 1; sc[h].t = n;
        if (!r && buf) sc[h].s = *buf;
        return r;
    }
    return stat(path, buf);
}

#define DYLD_INTERPOSE(_new, _old) \
    __attribute__((used, section("__DATA,__interpose"))) \
    static struct { void *n; void *o; } _interpose_##_old \
    = { (void *)&_new, (void *)&_old };

DYLD_INTERPOSE(my_access, access)
DYLD_INTERPOSE(my_stat, stat)

Then re-sign the claude binary to allow dylib injection (it ships with hardened runtime):

cat > /tmp/ents.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
    <key>com.apple.security.cs.allow-jit</key><true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
    <key>com.apple.security.cs.disable-library-validation</key><true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key><true/>
</dict></plist>
EOF

CLAUDE_BIN=$(ls -t ~/.local/share/claude/versions/* | head -1)
codesign -f -s - --entitlements /tmp/ents.plist "$CLAUDE_BIN"

Add to your wrapper script:

export DYLD_INSERT_LIBRARIES="$HOME/.local/lib/config_cache.dylib"

Note: you'll need to re-sign after claude updates.
</details>

@jeffgicharu your strace finding of 2152 madvise/sec is the same root cause as #2 above - the MIMALLOC_PURGE_DELAY=0 env var should fix the GC thrashing loop on Linux too.

Again this is all based on a few hours of poking around with Claude doing the heavy lifting on Bun internals, so Anthropic folks should definitely verify these findings properly. But the workarounds are working well for me.

JacekLach · 6 months ago

@sqdshguy thanks - the wrapper seems to have resovled the memory issue for me, where claude code would have previously gone up to 50G of ram

<img width="1252" height="124" alt="Image" src="https://github.com/user-attachments/assets/f95839de-0348-4079-b960-33c6fea9e46c" />

scriptogre · 6 months ago

Adding power consumption measurements that quantify the actual battery impact of this bug.

Environment: Claude Code 2.1.39, Node v20.20.0, macOS 15.7.1, MacBook Pro 16" M1 Pro, 16GB RAM

Power draw: each idle session burns a full CPU core

Using powermetrics (10-second sample), two completely idle Claude Code sessions — no active prompts, no streaming, no tool execution:

Name                               ID     CPU ms/s  User%  Deadlines (<2 ms, 2-5 ms)  Wakeups (Intr, Pkg idle)  Energy Impact
node (claude)                      14069  988.37    99.48  267.57  0.00               267.57  0.00              4002.01
node (claude)                      94710  988.97    99.51  241.21  0.00               241.21  0.00              3638.45

CPU Power: 10628 mW
Combined Power (CPU + GPU + ANE): 11290 mW

Each idle session uses 989ms of CPU per second (100% of one P-core at 3.2 GHz). Two sessions = ~10.6W CPU draw.

For comparison, the entire rest of the system — WebStorm, Brave Browser, iTerm2, WindowServer, Finder, and ~50 other processes — combined to less energy impact than a single idle Claude Code session.

Impact on battery life: Normal workload draws ~3-5W. Two idle Claude sessions add ~10W, roughly cutting battery life in half. Both processes had been running for 6 days:

PID     ELAPSED       COMMAND
14069   06-00:31:57   claude
94710   06-02:35:53   claude

Stack trace: TLS stream busy-wait loop

sample (5 seconds, 3800/3800 samples on the same stack — main thread never idles):

3800 node::SpinEventLoopInternal(node::Environment*)
  3800 uv_run  (in libuv.1.dylib)
    3800 uv__io_poll
      3800 uv__stream_io
        3800 node::crypto::TLSWrap::OnStreamRead(...)
          3800 node::crypto::TLSWrap::ClearOut()
            3800 node::EmitToJSStreamListener::OnStreamRead(...)
              3800 v8::internal::MicrotaskQueue::PerformCheckpoint(...)
                3800 v8::internal::MicrotaskQueue::RunMicrotasks(...)
                  3325 Builtins_RunMicrotasks
                    3257 Builtins_PromiseFulfillReactionJob
                      3201 Builtins_AsyncGeneratorAwaitResolveClosure

The event loop is stuck in a tight cycle: uv__io_poll → TLS stream read → promise fulfillment → async generator resume → microtask queue never drains → back to uv__io_poll which immediately fires again. The process never reaches kevent() to sleep.

This is a different code path from the uv__run_check/setImmediate pattern in #17148 and the GC thrashing (madvise/MADV_DODUMP) reported by @jeffgicharu — suggesting there may be multiple independent causes for the idle CPU spin, or the same root cause manifests differently depending on session state and platform.