[BUG] Memory allocation thrashing causes high sustained CPU usage when idle

Resolved 💬 3 comments Opened Jan 15, 2026 by magnetic-domelike-twisting Closed Jan 18, 2026

CLAUDE_CLI_PERFORMANCE_INVESTIGATION.md

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Claude CLI v2.1.7 exhibits severe performance degradation within 2-5 minutes of startup, consuming 50-80% CPU continuously even when completely idle (waiting for user input). This manifests as a tight memory allocation loop in the main event thread, allocating and immediately freeing 4KB pages at ~110 cycles per second.

Key symptoms:

  • Process starts normal (<5% CPU)
  • Within 2-5 minutes, climbs to 50-80% sustained CPU
  • Remains high even when idle for hours
  • Affects all sessions on all tested platforms
  • Cumulative I/O grows dramatically (42GB over 2.5 hours for one idle session)

What Should Happen?

Idle Claude CLI sessions should consume near-zero CPU (<1%) when waiting for user input, similar to other terminal applications.

Error Messages/Logs

**strace sampling (5 seconds) shows continuous mmap/munmap thrashing:**


% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 54.78    0.001065           1       540           munmap
 45.22    0.000879           1       560           mmap
------ ----------- ----------- --------- --------- ----------------
100.00    0.001944           1      1100           total


Rate: 110 mmap/munmap pairs per second, continuously.

**Pattern observed in strace:**


mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x787f93202000
mmap(NULL, 4096, ...) = 0x787f93201000
mmap(NULL, 4096, ...) = 0x787f93200000
munmap(0x787f93202000, 4096) = 0
munmap(0x787f93201000, 4096) = 0
munmap(0x787f93200000, 4096) = 0
[REPEATS INDEFINITELY]

Steps to Reproduce

  1. Install Claude CLI v2.1.7
  2. Start any new session: claude
  3. Wait 2-5 minutes (can interact normally or leave idle)
  4. In another terminal, check CPU: ps aux | grep claude
  5. Observe CPU climbing to 50-80%

To profile (Linux):

# Get Claude PID
PID=$(pgrep -f "^claude$" | head -1)

# Sample syscalls for 5 seconds
sudo strace -p $PID -e mmap,munmap -c

# Check I/O growth
cat /proc/$PID/io

Expected result: Minimal syscalls, low CPU usage
Actual result: 100+ mmap/munmap calls per second, 50-80% CPU

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

v2.1.7

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

Detailed Performance Metrics

Session A (2h 23min runtime, completely idle for final 2 hours):

Metric                      Value
--------------------------  ------------------------
CPU Time                    1h 55min (80.2% sustained)
Total I/O Read              42GB
Read Syscalls               2,440,996 (~283/sec avg)
Context Switches            2,640,905 voluntary
Memory                      962MB RSS (stable)
Threads                     23 (7 HeapHelper at 2.4% each)

Session B (16 minutes runtime, actively used):

Metric                      Value
--------------------------  ------------------------
CPU %                       53.6% sustained
Total I/O Read              3.5GB (~230MB/min)
Read Syscalls               627,720 (~650/sec)

Performance comparison (2-hour idle session):

Metric             Expected    Actual    Ratio
-----------------  ----------  --------  ------
CPU %              <1%         80%       80x
I/O Read           <10MB       42GB      4200x
Syscalls/sec       <100        ~2000+    20x

System Configuration

CPU:           Dual Intel Xeon E5-2660 v4 (56 threads, 28 cores)
RAM:           264GB
Architecture:  x86_64

[CLAUDE_CLI_PERFORMANCE_INVESTIGATION.md](https://github.com/user-attachments/files/24632573/CLAUDE_CLI_PERFORMANCE_INVESTIGATION.md)

eBPF Thread Profiling (5 seconds at 999Hz)

Thread             Samples    Percentage    Description
-----------------  ---------  ------------  ---------------------------
Main thread        1344       67.2%         Event loop spinning
HeapHelper (x7)    ~140 each  ~7% each      Heavy GC activity

Process State

wchan:         do_epoll_wait (supposedly idle)
Open files:    125 (normal)
Network:       13 HTTPS connections (all idle with 0 bytes queued)

Binary Analysis

Type:        ELF 64-bit, not stripped
Contains:    Node N-API symbols
Installed:   2026-01-13 17:00:48

Root Cause Hypothesis

The main event loop has a bug where:

  1. A 16ms timer fires continuously (60Hz)
  2. Callback allocates multiple 4KB pages
  3. Pages are immediately freed
  4. madvise() hints are applied
  5. Loop repeats indefinitely

This suggests the "terminal rendering optimization" in v2.1.7 is creating allocations on every frame tick, defeating the purpose of the optimization.

Claude CLI Performance Investigation

Date: 2026-01-14
Claude CLI Version: 2.1.7
Platform: Linux 6.17.4-2-pve
Investigator: Claude Sonnet 4.5

---

Executive Summary

Claude CLI version 2.1.7 exhibits a critical performance regression causing 50-80% sustained CPU usage on ALL sessions within minutes of startup, even when completely idle. The root cause is a memory thrashing loop in the main event thread continuously allocating and immediately freeing 4KB pages at ~110 cycles/second.

Impact:

  • System with 2 idle Claude sessions: 81.9% combined CPU usage
  • Single 2.5-hour session: 42GB cumulative I/O, 80% CPU
  • Single 16-minute session: 3.5GB I/O, 53.6% CPU

---

Investigation Methodology

Tools Used

  • perf - CPU profiling with call stacks
  • bpftrace - eBPF-based thread sampling and syscall tracing
  • strace - System call tracing
  • lsof - Open file descriptor analysis
  • iotop - I/O monitoring
  • Standard Linux process tools (ps, top, /proc)

Sessions Profiled

  1. Session A (PID 959052) - 2h 23min old, transcribe-o-matic project
  2. Session B (PID 1537870) - 16min old, current session

---

Root Cause: Memory Thrashing Loop

Symptoms

Continuous mmap/munmap cycling:

mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x787f93202000
mmap(NULL, 4096, ...) = 0x787f93201000
mmap(NULL, 4096, ...) = 0x787f93200000
mmap(NULL, 4096, ...) = 0x787f8a801000
munmap(0x787f93202000, 4096) = 0
munmap(0x787f93201000, 4096) = 0
munmap(0x787f93200000, 4096) = 0
munmap(0x787f8a801000, 4096) = 0
[REPEATS CONTINUOUSLY]

Measured Rates

5-second strace sample (Session B, 16 minutes old):

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 54.78    0.001065           1       540           munmap
 45.22    0.000879           1       560           mmap
------ ----------- ----------- --------- --------- ----------------
100.00    0.001944           1      1100           total

Rate: 110 mmap/munmap pairs per second
Syscall overhead alone: ~50% CPU

Syscall Distribution (3-second sample)

Syscall              % Time   Calls   Description
------------------   ------   -----   -----------
futex                67.01    257     Thread synchronization
clock_gettime        17.66    297     Time polling
sched_yield          16.14     38     Yielding CPU
epoll_pwait2         16.02     36     Event waiting

Plus hundreds of mmap/munmap not shown in filtered output.

---

Secondary Issues

1. Aggressive Memory Stats Monitoring

File descriptor 10 open to: /proc/PID/statm

pread64(10, "22390468 237431 9149 14992 0 199...", 256, 0)
  • Read continuously during active work
  • Contributes to excessive I/O
  • Not observed in idle state but likely triggered periodically

2. MCP Server Connection Issues

Observed in debug logs:

2026-01-15T04:00:12.423Z [DEBUG] MCP server "context7": HTTP connection dropped after 9002s uptime
2026-01-15T04:00:12.423Z [DEBUG] MCP server "context7": Connection error: SSE stream disconnected: TimeoutError
2026-01-15T04:00:13.434Z [DEBUG] MCP server "context7": Token expired without refresh token

Pattern:

  • Connection drops every ~2.4 hours (8700-9000 seconds)
  • Continuous reconnection attempts
  • OAuth token refresh failures

3. Timer Thrashing

16ms timer loop:

timerfd_settime(7, 0, {it_interval={tv_sec=0, tv_nsec=16000000}}, NULL)
epoll_ctl(4, EPOLL_CTL_ADD, 7, {events=EPOLLIN, data=0x35e820b00c0}) = -1 EEXIST
read(7, "\1\0\0\0\0\0\0\0", 8)
[REPEATS EVERY 16ms]

Timer fires continuously even when idle, likely driving the mmap/munmap loop.

4. Garbage Collection Pressure

Thread CPU distribution (Session B, 16 minutes old):

THREAD           CPU%    TIME
--------------   ----    --------
claude (main)    33.0    00:05:35
HeapHelper       2.5     00:00:26
HeapHelper       2.5     00:00:25
HeapHelper       2.4     00:00:25
HeapHelper       2.4     00:00:25
HeapHelper       2.4     00:00:25
HeapHelper       2.4     00:00:24
HeapHelper       2.4     00:00:24

7 HeapHelper threads at ~2.4% each = 16.8% additional CPU overhead from GC.

---

Detailed Metrics

Session A (PID 959052) - 2h 23min Runtime

| Metric | Value | Notes |
|--------|-------|-------|
| CPU Time | 1h 55min | 80.2% sustained |
| Wall Time | 2h 23min | Since 18:30 |
| Total I/O Read | 42GB | Cumulative |
| Total I/O Write | 1.6GB | Cumulative |
| Read Syscalls | 2,440,996 | ~283/sec average |
| Write Syscalls | 754,387 | ~87/sec average |
| Context Switches (voluntary) | 2,640,905 | I/O waits |
| Context Switches (involuntary) | 85,183 | Preemption |
| Memory RSS | 962MB | Stable |
| Memory VSZ | 85.4GB | Virtual |
| Threads | 23 | Including 7 HeapHelper |
| Open Files | 125 | Normal |
| Network Connections | 13 HTTPS | All idle |

Process State: Sleeping (waiting on do_epoll_wait)
Last Activity: Stream started 03:55:06 (over 2 hours idle)

Session B (PID 1537870) - 16min Runtime

| Metric | Value | Notes |
|--------|-------|-------|
| CPU % | 53.6% | Sustained |
| Wall Time | 16 minutes | Since 20:48 |
| Total I/O Read | 3.5GB | ~230MB/min |
| Total I/O Write | 162MB | ~10MB/min |
| Read Syscalls | 627,720 | ~650/sec |
| Write Syscalls | 266,955 | ~278/sec |
| Memory RSS | 578MB | Growing |
| Memory VSZ | 73GB | Virtual |
| Threads | 25 | Including 7 HeapHelper |

Process State: Running/Sleeping (active work)
Current Activity: Profiling and investigation

System-Wide Impact

Total Claude CPU: 81.9%
Total Claude MEM: 0.3%
Total Sessions: 169 directories in ~/.claude/session-env/
Debug Logs: 3.9MB across 22 files >500KB

---

Performance Comparison

Expected vs Actual (2-hour idle session)

| Metric | Expected | Actual | Ratio |
|--------|----------|--------|-------|
| CPU % | <1% | 80% | 80x |
| CPU Time | <1 min | 115 min | 115x |
| I/O Read | <10MB | 42GB | 4200x |
| Syscalls/sec | <100 | ~2000+ | 20x |
| Context Switches | <10k | 2.7M | 270x |

---

perf Profile Analysis

Top CPU Consumers (Session A, 5-second sample)

Children    Self    Samples  Command   Shared Object        Symbol
--------  ------  --------  --------  -------------------  ------
  58.95%   0.00%        0   claude    [unknown]            [.]
  29.40%                    └── 0x2df6405
    29.27%                      └── 0x2fc803a
      26.01%                      └── 0x71465ba2413e
        15.68%                      └── 0x71465e8c619d
          14.30%                      └── 0x71465f8dd4e2

Symbols not resolved (stripped binary), but heavy call stack activity in event loop.

eBPF Thread Sampling (Session A, 5 seconds at 999Hz)

Thread ID    Samples    %
---------    -------    ----
959052       1344       67.2%   (Main thread)
1329442      140        7.0%    (HeapHelper)
1329444      140        7.0%    (HeapHelper)
1329446      139        6.9%    (HeapHelper)
1329452      137        6.9%    (HeapHelper)
1329448      133        6.7%    (HeapHelper)
1329453      133        6.7%    (HeapHelper)
1329450      121        6.0%    (HeapHelper)
1330225      31         1.5%    (JITWorker)

Main thread dominates, HeapHelpers show continuous GC activity.

eBPF Wakeup Analysis (5 seconds)

@wakeups[claude]:      29
@wakeups[HeapHelper]:  27
@wakeups[HTTP Client]: 9
@wakeups[JITWorker]:   2

High wakeup rate even when idle.

---

Debug Log Analysis

Sample Log Entry (Session A)

File: ~/.claude/debug/69bac4ba-0683-471b-a74b-845540b85e29.txt
Size: 1.4MB
Pattern: Repeated MCP connection errors

2026-01-15T03:55:12.419Z [DEBUG] MCP server "context7": HTTP connection dropped after 8702s
2026-01-15T03:55:12.419Z [DEBUG] MCP server "context7": Connection error: SSE stream disconnected
2026-01-15T03:55:13.422Z [DEBUG] MCP server "context7": Token expired without refresh token

Frequency: Connection drops every ~2.4 hours, reconnection attempts continuous.

Log Bloat Statistics

Logs over 500KB: 22 files
Total debug size: 3.9MB
Cache size: 1.0MB

---

Network Analysis

Active Connections (Session A)

All connections in ESTABLISHED state with 0 bytes in send/receive queues:

saturn:56998 -> 160.79.104.10:443    (anthropic.com)
saturn:52534 -> 18.217.33.196:443    (AWS)
saturn:37134 -> 34.149.66.137:443    (Google Cloud)
[10 more connections to 160.79.104.10:443]

All idle, waiting for data.

---

Binary Analysis

Claude CLI Binary:

Path: /home/tyler/.local/share/claude/versions/2.1.7
Type: ELF 64-bit LSB executable
Architecture: x86-64
Build ID: 2a543e710b8a24fed9319312d8057c9f707d9578
Symbols: Not stripped
Node API: Present (node_api_create_*, node_api_symbol_for)
Installed: 2026-01-13 17:00:48 (1 day ago)

Binary appears to be Node.js-based (N-API symbols present).

---

Reproduction Steps

  1. Start any new Claude CLI session: claude
  2. Wait 2-5 minutes
  3. In another terminal, monitor: watch -n2 'ps aux | grep claude | grep -v grep'
  4. Observe CPU climbing from <5% to 50-80%
  5. Profile with: strace -p $(pgrep -f "claude$" | head -1) -e mmap,munmap -c
  6. Observe continuous mmap/munmap cycling

Reproducibility: 100% on all sessions tested

---

Technical Diagnosis

Likely Root Cause

The main event loop appears to have a memory allocation leak/bug causing it to:

  1. Request 4KB pages continuously
  2. Immediately free them
  3. Repeat indefinitely at ~110Hz

This suggests:

  • Memory pool corruption
  • Allocator thrashing from fragmentation
  • Event loop callback leak
  • Timer callback creating allocations on every tick

Event Loop Architecture Issues

16ms timer fires
└── Callback allocates memory
    └── Memory immediately freed
        └── madvise() hints added
            └── Timer resets for 16ms
                └── [LOOP]

The 16ms timer (60Hz) combined with allocation per callback explains the ~110Hz allocation rate (including multiple allocations per tick).

Additional Data

Environment Details

OS: Linux 6.17.4-2-pve
Distro: Proxmox (Debian-based)
Architecture: x86_64
Memory: 257GB total, 218GB used
CPU: Multi-core (load average: 8.21, 8.45, 8.57)
Working Directory: /share/temp/transcribe-o-matic

Process Tree (Session A)

claude,959052
├── npm exec @upsta,959276          (context7-mcp launcher)
│   └── node,959505                  (context7-mcp server)
│       └── 6 node threads
├── socat,960128                     (HTTP proxy)
├── socat,960129                     (SOCKS proxy)
└── 23 claude threads:
    ├── Bun Pool (10 threads)
    ├── HTTP Client (1 thread)
    ├── File Watcher (1 thread)
    ├── HeapHelper (7 threads) ← High CPU
    ├── JITWorker (1 thread)
    └── Main threads (3 threads) ← Very high CPU

---

Appendix: Raw Command Outputs

Session A Profile Commands

# Process stats
ps aux | grep 959052
# USER    PID %CPU %MEM    VSZ   RSS TTY  STAT START   TIME COMMAND
# tyler 959052 80.4 0.3 89561872 955332 pts/0 Sl+ 18:30 112:54 claude

# I/O stats
cat /proc/959052/io
# rchar: 42494217939
# wchar: 1647217649
# syscr: 2440996
# syscw: 754387
# read_bytes: 2073088
# write_bytes: 77723349

# Context switches
cat /proc/959052/status | grep ctxt
# voluntary_ctxt_switches:	2640905
# nonvoluntary_ctxt_switches:	85183

# perf record
sudo perf record -p 959052 -g -F 99 -o /tmp/claude/perf-959052.data -- sleep 5
# [ perf record: Captured and wrote 0.191 MB (423 samples) ]

Session B Profile Commands

# strace syscall distribution
sudo timeout 3 strace -p 1537870 -c
#  28.28%  sched_yield       2055 calls
#   2.44%  wait4             2 calls
#   1.07%  epoll_pwait2      208 calls
#   0.66%  madvise           282 calls

# mmap/munmap rate
sudo timeout 5 strace -p 1537870 -e mmap,munmap -c
# 54.78%  munmap  540 calls
# 45.22%  mmap    560 calls

# Thread CPU sampling
sudo timeout 2 bpftrace -e 'profile:hz:99 /pid == 1537870/ { @[tid] = count(); }'
# @[1537870]: 128  (main thread dominates)

---

Report Generated: 2026-01-14 21:06 MST
Investigation Duration: ~1 hour
Tools Used: perf, bpftrace, strace, lsof, iotop, /proc analysis

View original on GitHub ↗

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