Native heap leak: 1.15B identical 176-byte objects (189GB) in worker process during active tool use
Summary
Claude Code worker process leaks ~189GB of native heap memory over a 10-hour active session. The leak consists of ~1.15 billion identical 176-byte malloc chunks on the glibc [heap], entirely outside Bun's mimalloc-managed memory.
Workaround
We built an LD_PRELOAD guard that caps the leak. No binary modification needed.
👉 https://github.com/dalsoop/claude-code-memory-leak-fix
git clone https://github.com/dalsoop/claude-code-memory-leak-fix.git
cd claude-code-memory-leak-fix
make && sudo make install
claude-safe # drop-in replacement for 'claude'
- Below 10GB: zero overhead (passthrough)
- Above 10GB: pools leaked 160-byte objects to stop heap growth
kill -USR1 $(pgrep -n claude)to check stats anytime
Testing confirmed free() is never called for these objects:
[malloc-guard] 160B alloc=17 free=0 live=17 pool_hit=0 guard=standby
Environment
- Claude Code: 2.1.81 (Bun v1.2 SEA binary)
- Runtime: Bun v1.2 (JavaScriptCore, NOT Node.js/V8)
- Allocator: mimalloc (mmap) + glibc malloc (brk/[heap]) dual allocator
- Static-linked: BoringSSL, picohttpparser, nghttp2, lshpack
- Dynamic-linked: libc only
- OS: Proxmox VE / Debian Trixie, Linux 6.17.2
- RAM: 503GB (64-core AMD Threadripper 3960X)
Key Finding: Dual Allocator Architecture
Claude Code (Bun) uses mimalloc internally but does NOT override glibc's malloc globally. This means:
- Bun's own code → mimalloc → mmap regions (3,236 segments, normal)
- Static-linked C libraries (BoringSSL, nghttp2, etc.) → glibc malloc →
[heap]via brk/sbrk
The leak is entirely in the glibc [heap] region, meaning it originates from static-linked C library code, not from JavaScript or Bun's managed memory.
Evidence
Memory layout from /proc/pid/smaps
38485000-2f95407000 rw-p 00000000 00:00 0 [heap]
Rss: 198655488 kB (189.5 GB)
Private_Dirty: 198655488 kB
Anonymous: 198655488 kB
Repeating 176-byte malloc chunk
Every sampled page across the 189GB heap contains the same repeating structure:
Offset Hex ASCII
0: b1 00 00 00 00 00 00 00 7c 78 65 ff 00 00 00 01 ........|xe.....
16: 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
32: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
48: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
... (all zeros to byte 176)
- malloc chunk size:
0xb1= 0xb0 + PREV_INUSE flag → 160 bytes user data - Constant value at offset 0:
0x01000000ff65787c(identical across all 1.15B objects) - Field at offset 8:
4(possible type/enum) - Field at offset 24:
1(possible refcount) - 136 bytes (85%) of each object are zeros
Quantification
| Metric | Value |
|--------|-------|
| Sampled pages | 200 random pages across 189GB |
| Avg chunks per 4KB page | 23.2 |
| Estimated total chunks | ~1,150,000,000 |
| Estimated total size | 189GB |
| Growth rate | ~32,000 objects/sec over 10 hours |
Cross-process comparison
| Process | Role | Uptime | Heap RSS |
|---------|------|--------|----------|
| 669419 | wrapper (parent) | 10 hours | 53 MB ✅ |
| 669645 | worker (active tool use) | 10 hours | 189 GB ❌ |
| 1054383 | worker (idle, 30 min) | 30 min | 32 KB ✅ |
- Leak only occurs in worker process during active tool use
- Idle sessions show zero heap growth (verified with 30-second measurement)
Binary analysis
$ file claude
ELF 64-bit LSB executable, x86-64, not stripped
$ readelf -s claude | grep BUN
BUN_1.2 version symbols throughout
$ strings claude | grep mimalloc
mimalloc (8 occurrences)
$ strings claude | grep "Welcome to Bun"
Welcome to Bun v during preload.
$ ldd claude
libc.so.6 only (+ librt, libpthread, libdl, libm)
Suspected leak path
tool call → Claude API request → HTTP/2 streaming response
→ nghttp2 or BoringSSL internal buffer allocation (glibc malloc)
→ buffer never freed → [heap] grows via brk/sbrk
The 160-byte object size matches uv_poll_t and uv_getaddrinfo_t (both 160 bytes on this system). However, the field layout doesn't align with standard libuv — Bun uses its own libuv fork with potentially different struct layouts.
Relationship to existing issues
This is related to #33589 and #33915 but represents a different leak path:
| | #33589 / #33915 | This issue |
|--|----------------|------------|
| Runtime assumed | Node.js / V8 | Bun / JavaScriptCore |
| Where | V8 heap (ArrayBuffers) | glibc [heap] (brk region) |
| Allocator | V8's allocator | glibc malloc (bypasses mimalloc) |
| Object type | ArrayBuffer backing stores | Fixed 160-byte C structs |
| Detectable by | --inspect + heapdump | Only /proc/pid/smaps + memory dump |
| Mitigated by | --max-old-space-size | Nothing — outside both JSC and mimalloc |
Reproduction
- Start
claudein terminal - Use it actively with many tool calls over several hours
- Monitor:
watch -n 60 'grep VmRSS /proc/$(pgrep -n claude)/status' - Observe unbounded RSS growth in
[heap]region
Suggested investigation
Since the leak is in glibc malloc (not mimalloc), it must originate from static-linked C code. Candidates:
- nghttp2: HTTP/2 frame buffers during streaming responses
- BoringSSL: TLS session buffers
- Bun's libuv fork: handle/request objects allocated via glibc malloc
Profiling with MALLOC_TRACE, mtrace(), or LD_PRELOAD malloc interceptor on the live process would identify the exact allocation callsite.
🤖 Generated with Claude Code
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗