Memory leak: API Response bodies (~181 MB each) accumulate per turn and are never GC'd
Summary
Each API turn leaks ~181 MB — a Response object backed by the Anthropic API streaming body is retained by an unsettled PromiseReaction chain and never released. After 7 turns (~5.5 min) the process holds 1.27 GB of unreleased response bodies.
Environment
- ccVersion: 2.1.72
- nodeVersion: v24.3.0 (Bun runtime)
- platform: darwin
Evidence: Two heap snapshots compared
Snapshot 1 — 6.7s uptime:
- heapUsed: 325 MB, rss: 310 MB, growthRate: 45.8 MB/s (stream in-flight)
- 1 x native Response object, 181 MB
Snapshot 2 — 332s uptime (~5.5 min later):
- heapUsed: 8.1 GB (reported), rss: 585 MB, growthRate: 1.76 MB/s
- 7 x native Response objects, each exactly 185,951 KB = 1.27 GB total
- 25,502 weakly-held ArrayBuffers (SSE chunk buffers, GC-pending)
Retainer chain (identical for all 7 leaked instances)
PromiseReaction (hidden)
-> Generator (suspended, never completing)
-> JSLexicalEnvironment
- self: IncomingMessage
- stream: IncomingMessage
- (closure) -> JSLexicalEnvironment { response: Response [181 MB] }
-> IncomingMessage.FetchAPI -> Response [181 MB body]
The async iterator reading the SSE stream leaves a suspended Generator retained by a PromiseReaction. The Generator's closure holds IncomingMessage and Response alive indefinitely. One new 181 MB instance leaks per turn.
Impact
- RSS grows ~39 MB/turn (physical), virtual allocation grows ~181 MB/turn
- Long sessions (20+ turns) will exhaust memory on typical developer machines
Steps to reproduce
- Start a new Claude Code session
- Run /heapdump immediately — note 1 x ~181 MB Response
- Have several back-and-forth turns
- Run /heapdump again — one new ~181 MB Response per turn retained
Expected behavior
After a streaming response is fully consumed and the turn completes, the Response body and IncomingMessage should be released and become eligible for GC.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗