claude-code - 💡(How to fix) Fix Memory leak: Buffer.slice()/subarray() in MCP IPC creates retained views instead of copies [2 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#48647Fetched 2026-04-16 06:54:47
View on GitHub
Comments
2
Participants
1
Timeline
9
Reactions
0
Author
Participants
Timeline (top)
labeled ×4commented ×2cross-referenced ×2closed ×1

Buffer.slice() and Buffer.subarray() in MCP stdio IPC handlers create views (not copies) that retain the entire underlying ArrayBuffer. This causes monotonic memory growth proportional to MCP server throughput.

Root Cause

Two hot paths in the Bun-compiled claude.exe binary (v2.1.109) use view-creating operations on Buffers:

Fix Action

Fix

Replace view operations with copy operations:

// Bridge socket
let A = Buffer.from(this.responseBuffer.slice(4, 4 + f));  // copy
this.responseBuffer = Buffer.from(this.responseBuffer.slice(4 + f));  // copy

// ReadBuffer
this._buffer = Buffer.from(this._buffer.subarray(H + 1));  // copy

The copy cost is O(remaining_buffer) per message but eliminates the retention chain. Alternatively, use a ring buffer or pre-allocated pool.

Code Example

// .bun section offset ~174310
socket.on("data", (_) => {
  this.responseBuffer = Buffer.concat([this.responseBuffer, _]);
  while (this.responseBuffer.length >= 4) {
    let f = this.responseBuffer.readUInt32LE(0);
    if (this.responseBuffer.length < 4 + f) break;
    let A = this.responseBuffer.slice(4, 4 + f);       // VIEW - retains entire buffer
    this.responseBuffer = this.responseBuffer.slice(4 + f); // VIEW - retains entire buffer
    // ... JSON.parse(A.toString("utf-8")) ...
  }
});

---

// .bun section offset ~708947
class pgH {
  append(H) { this._buffer = this._buffer ? Buffer.concat([this._buffer, H]) : H }
  readMessage() {
    if (!this._buffer) return null;
    let H = this._buffer.indexOf("\n");
    if (H === -1) return null;
    let $ = this._buffer.toString("utf8", 0, H).replace(/\r$/, "");
    return this._buffer = this._buffer.subarray(H + 1), AX7($);  // VIEW
  }
}

---

// Bridge socket
let A = Buffer.from(this.responseBuffer.slice(4, 4 + f));  // copy
this.responseBuffer = Buffer.from(this.responseBuffer.slice(4 + f));  // copy

// ReadBuffer
this._buffer = Buffer.from(this._buffer.subarray(H + 1));  // copy
RAW_BUFFERClick to expand / collapse

Summary

Buffer.slice() and Buffer.subarray() in MCP stdio IPC handlers create views (not copies) that retain the entire underlying ArrayBuffer. This causes monotonic memory growth proportional to MCP server throughput.

Analysis

Two hot paths in the Bun-compiled claude.exe binary (v2.1.109) use view-creating operations on Buffers:

1. Bridge socket MCP handler (class vJ$)

// .bun section offset ~174310
socket.on("data", (_) => {
  this.responseBuffer = Buffer.concat([this.responseBuffer, _]);
  while (this.responseBuffer.length >= 4) {
    let f = this.responseBuffer.readUInt32LE(0);
    if (this.responseBuffer.length < 4 + f) break;
    let A = this.responseBuffer.slice(4, 4 + f);       // VIEW - retains entire buffer
    this.responseBuffer = this.responseBuffer.slice(4 + f); // VIEW - retains entire buffer
    // ... JSON.parse(A.toString("utf-8")) ...
  }
});

Buffer.slice() returns a view sharing the same ArrayBuffer. The parsed message A holds a reference to the entire concatenated buffer. Even after this.responseBuffer is reassigned to a new slice, the original memory cannot be GC'd until all slice references (including A passed to JSON.parse internals) are released.

2. ReadBuffer class (pgH) for MCP JSON-RPC line protocol

// .bun section offset ~708947
class pgH {
  append(H) { this._buffer = this._buffer ? Buffer.concat([this._buffer, H]) : H }
  readMessage() {
    if (!this._buffer) return null;
    let H = this._buffer.indexOf("\n");
    if (H === -1) return null;
    let $ = this._buffer.toString("utf8", 0, H).replace(/\r$/, "");
    return this._buffer = this._buffer.subarray(H + 1), AX7($);  // VIEW
  }
}

subarray() also creates a view. Every readMessage() call leaves the old buffer retained.

Impact

Each active MCP server connection has both a bridge socket and ReadBuffer. With N MCP servers (typically 3-7), retained buffer chains grow proportionally to total bytes received. For tool calls returning large payloads (file contents, search results), individual retained buffers can be megabytes.

Over a multi-hour session with heavy tool use, this is a likely contributor to the 5-10 GB private memory growth observed in #42169, #33735, #47711.

Fix

Replace view operations with copy operations:

// Bridge socket
let A = Buffer.from(this.responseBuffer.slice(4, 4 + f));  // copy
this.responseBuffer = Buffer.from(this.responseBuffer.slice(4 + f));  // copy

// ReadBuffer
this._buffer = Buffer.from(this._buffer.subarray(H + 1));  // copy

The copy cost is O(remaining_buffer) per message but eliminates the retention chain. Alternatively, use a ring buffer or pre-allocated pool.

Environment

  • claude.exe v2.1.109 (Bun binary)
  • Analysis method: PE section extraction + pattern search on .bun JS bundle
  • Affects all platforms (Buffer.slice/subarray semantics are the same in Node.js and Bun)

Related Issues

  • #42169 (Windows resource exhaustion)
  • #33735 (18 GB private memory)
  • #47711 (930 MB/min growth)
  • #33589 (BytesInternalReadableStreamSource — separate issue in fetch layer)
  • #33447 (API response body retention — related but different path)

extent analysis

TL;DR

Replace Buffer.slice() and Buffer.subarray() with Buffer.from() to create copies instead of views, eliminating the memory retention issue.

Guidance

  • Identify all occurrences of Buffer.slice() and Buffer.subarray() in the codebase, particularly in performance-critical paths like the bridge socket MCP handler and ReadBuffer class.
  • Replace these view-creating operations with Buffer.from() to create copies, as shown in the proposed fix.
  • Consider using a ring buffer or pre-allocated pool as an alternative solution to minimize the copy cost.
  • Verify the fix by monitoring memory usage over time, especially during heavy tool use or large payload transfers.

Example

// Before
let A = this.responseBuffer.slice(4, 4 + f);
this.responseBuffer = this.responseBuffer.slice(4 + f);

// After
let A = Buffer.from(this.responseBuffer.slice(4, 4 + f));
this.responseBuffer = Buffer.from(this.responseBuffer.slice(4 + f));

Notes

The proposed fix may introduce a performance overhead due to the copying of buffers. However, this is a necessary trade-off to prevent the memory retention issue. Additionally, the use of Buffer.from() ensures that the copied buffers are properly garbage collected, preventing memory leaks.

Recommendation

Apply the workaround by replacing Buffer.slice() and Buffer.subarray() with Buffer.from() to create copies, as this directly addresses the root cause of the memory retention issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Memory leak: Buffer.slice()/subarray() in MCP IPC creates retained views instead of copies [2 comments, 1 participants]