Memory leak: 6GB of weakly-held ArrayBuffers not freed (native backing stores)

Resolved 💬 3 comments Opened Mar 12, 2026 by TweedBeetle Closed Mar 12, 2026

Description

Claude Code leaks ~5 MB/s of native memory through ArrayBuffer backing stores that are never freed. A session that ran for only 106 seconds accumulated 6.7 GB of memory, almost entirely in externally-backed ArrayBuffers.

Environment

  • Claude Code: 2.1.72
  • Node.js: v24.3.0
  • Platform: macOS (Darwin 25.1.0)
  • ~60 MCP servers configured

Evidence from heap snapshot analysis

Diagnostics summary (from built-in heap dump)

{
  "uptimeSeconds": 106,
  "memoryUsage": {
    "heapUsed": 6746204797,
    "heapTotal": 57854976,
    "external": 6699911341,
    "arrayBuffers": 6250945148,
    "rss": 534560768
  },
  "memoryGrowthRate": {
    "bytesPerSecond": 5024999,
    "mbPerHour": 17251
  }
}

Key observation: V8 heap is only 58 MB, but external memory is 6.7 GB. Almost all of it (6.25 GB) is in arrayBuffers.

Programmatic heap snapshot analysis

Parsed the .heapsnapshot file with a Node.js script. Findings:

1. 11,479 ArrayBuffers at uniform ~512KB size (5,741 MB total)

| Size bucket | Count | Total MB |
|---|---|---|
| 512KB - 1MB | 11,479 | 5,741 |
| 100KB - 512KB | 799 | 200 |
| >10MB | 1 | 16 (WebAssembly.Memory, expected) |

2. All large ArrayBuffers are only weakly reachable from the GC root

=== OBJECTS REFERENCING ARRAYBUFFERS (by type::name) ===
  5940.94 MB | 12278 refs | synthetic::(root)  [via weak reference]
  16.00 MB   | 1 refs     | code::JSLexicalEnvironment  [WASM, expected]

No JS object holds a strong reference to these buffers. They are retained only by synthetic::(root) via weak edges. Despite being weakly held in V8, the native backing stores are never freed because V8/JSC does not control external memory deallocation.

3. Uniform ~512KB size suggests stdio transport reads

The consistent buffer size points to stdout.on('data') chunks from MCP server stdio transports. Each chunk allocates a native-backed ArrayBuffer. With many MCP servers, these accumulate rapidly.

4. Separate leak: 181 MB unconsumed HTTP Response

A single Response object (181 MB) is retained by a pending Promise chain:

Response (181 MB)
  <- IncomingMessage via "FetchAPI"
  <- JSLexicalEnvironment via "response"
    <- ... closures via "stream"/"src"
      <- Generator
        <- PromiseReaction
          <- Promise (pending)
            <- Object via "end"  (onend callback never fired)

This is an HTTP response whose stream was never fully consumed, so the body stays in memory indefinitely.

Uint8Array retention pattern

Uint8Arrays >= 1KB: 58
  46 refs | JSLexicalEnvironment [chunk]
  46 refs | Readable [remainingChunk]
  4 refs  | NativeReadableStreamSource [data]

46 Readable streams hold remainingChunk references to Uint8Array views over the leaked ArrayBuffers, confirming the stream/transport origin.

Reproduction

  1. Configure many MCP servers (stdio transport)
  2. Start a Claude Code session
  3. Monitor process.memoryUsage().arrayBuffers - it grows at ~5 MB/s
  4. After ~2 minutes, memory exceeds 6 GB

Likely root cause

The MCP stdio transport reads chunks from child process stdout. Each read creates an externally-backed ArrayBuffer. After the chunk is processed, the JS reference is dropped, but:

  • The ArrayBuffer's native backing store is not freed because the allocator (Node.js Buffer pool or libuv) retains ownership
  • V8's GC sees the wrapper as weakly reachable but cannot trigger deallocation of external memory
  • On Node v24 (with JavaScriptCore on macOS), this may be exacerbated by different GC heuristics for external memory pressure

Related issues

  • #19317 - Memory leaks cause system crashes
  • #11155, #18859 - Multiple Claude processes leak memory

View original on GitHub ↗

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