Native heap leak: 1.15B identical 176-byte objects (189GB) in worker process during active tool use

Resolved 💬 6 comments Opened Mar 21, 2026 by dalsoop Closed Apr 18, 2026

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

  1. Start claude in terminal
  2. Use it actively with many tool calls over several hours
  3. Monitor: watch -n 60 'grep VmRSS /proc/$(pgrep -n claude)/status'
  4. 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

View original on GitHub ↗

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