[BUG] Memory allocation thrashing causes high sustained CPU usage when idle
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
- Install Claude CLI v2.1.7
- Start any new session:
claude - Wait 2-5 minutes (can interact normally or leave idle)
- In another terminal, check CPU:
ps aux | grep claude - 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:
- A 16ms timer fires continuously (60Hz)
- Callback allocates multiple 4KB pages
- Pages are immediately freed
- madvise() hints are applied
- 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 stacksbpftrace- eBPF-based thread sampling and syscall tracingstrace- System call tracinglsof- Open file descriptor analysisiotop- I/O monitoring- Standard Linux process tools (
ps,top,/proc)
Sessions Profiled
- Session A (PID 959052) - 2h 23min old, transcribe-o-matic project
- 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
- Start any new Claude CLI session:
claude - Wait 2-5 minutes
- In another terminal, monitor:
watch -n2 'ps aux | grep claude | grep -v grep' - Observe CPU climbing from <5% to 50-80%
- Profile with:
strace -p $(pgrep -f "claude$" | head -1) -e mmap,munmap -c - 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:
- Request 4KB pages continuously
- Immediately free them
- 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
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗