Memory leak: self-upgrade fetch Response body not released, causing 45GB+ external memory accumulation (Bun ArrayBuffer finalizer)
Bug Description
Claude Code experiences severe memory leak during self-upgrade, accumulating 45+ GB of external memory within ~53 minutes of runtime. The JS heap itself stays small (~88MB), but external memory grows at ~9.2 GB/hour due to unreleased Bun native ArrayBuffer backing stores.
Environment
- Platform: darwin (macOS, Apple Silicon arm64)
- Node/Runtime: Bun (Node.js v24.3.0 compat)
- Claude Code Version: 2.1.71 (upgrading to 2.1.74)
- Uptime at dump: ~53 minutes
- Trigger: Manual heapdump via
/heapdump
Memory Stats at Time of Dump
| Metric | Value |
|---|---|
| heapUsed | 49,084,759,975 bytes (~45.7 GB) |
| heapTotal | 92,032,000 bytes (~88 MB) |
| external | 49,011,553,216 bytes (~45.6 GB) |
| arrayBuffers | 48,027,108,427 bytes (~44.7 GB) |
| rss | 8,588,787,712 bytes (~8 GB) |
| Memory growth rate | 9,191.9 MB/hour |
Root Cause (from heapsnapshot analysis)
The heap snapshot contains 116,640 ArrayBuffer objects totaling ~44.7 GB:
| ArrayBuffer size | Count | Total |
|---|---|---|
| 524,432 bytes (512KB + 144) | 66,263 | 32.4 GB |
| 262,288 bytes (256KB + 144) | 50,363 | 12.3 GB |
The +144 byte overhead is Bun's internal object header for ReadableStream chunk buffers.
Retainer chain
There are 3 live native:Response objects (~545 MB self_size each), all pointing to the self-upgrade download URL:
https://storage.googleapis.com/.../claude-code-releases/2.1.74/darwin-arm64/claude
Retainer path:
IncomingMessage.FetchAPI → native:Response
JSLexicalEnvironment(context:'response') → native:Response → ReadableStream → ArrayBuffer[]
The response variable is captured in a closure and the body stream is never consumed or cancelled. Since Bun's GC is not triggered aggressively (JS heap is tiny, never hits GC threshold), the native ArrayBuffer backing stores' finalizers are never called, so memory accumulates indefinitely.
Estimated 300+ historical fetch calls left unreleased (116,626 chunks ÷ ~350 chunks/download ≈ 333 fetches), suggesting the self-upgrade check/download loop runs repeatedly.
Reproduction
- Run Claude Code for ~1 hour in a session where self-upgrade checks are active
- Run
/heapdump - Observe
externalmemory >>heapTotal
Expected vs Actual
| | Expected | Actual |
|---|---|---|
| Memory after self-upgrade fetch | Response body consumed/cancelled, memory released | Response body never consumed/cancelled, 44+ GB accumulates |
Suggested Fix
In the self-upgrade fetch code, ensure the response body is always consumed or explicitly cancelled:
const response = await fetch('https://storage.googleapis.com/.../claude');
// Option A: consume the body
const data = await response.arrayBuffer();
// Option B: if body isn't needed, cancel it explicitly
await response.body?.cancel();
response = null;
Additionally, consider calling Bun.gc(true) after large downloads to force synchronous GC and trigger finalizers promptly.
Diagnostics files
Heap snapshot (60MB) and diagnostics JSON were generated via /heapdump. Key diagnostics JSON attached below:
{
"timestamp": "2026-03-13T05:14:31.797Z",
"uptimeSeconds": 3207.947879375,
"memoryUsage": {
"heapUsed": 49084759975,
"heapTotal": 92032000,
"external": 49011553216,
"arrayBuffers": 48027108427,
"rss": 8588787712
},
"memoryGrowthRate": {
"bytesPerSecond": 2677346.40,
"mbPerHour": 9191.94
},
"v8HeapStats": {
"heapSizeLimit": 68719476736,
"mallocedMemory": 49086333440
},
"ccVersion": "2.1.71",
"nodeVersion": "v24.3.0",
"platform": "darwin"
}This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗