Repository navigation
Blob.prototype.stream() memory-leaks the source buffer #63574
Description
Activity
Reader::wakeup_is keeping the stream reference alive (cc @jasnell)$ out/Release/node --expose-gc test.js arrayBuffers: 201.0 MiB $ out/Release/node-no-blob-reader-wakeup --expose-gc test.js arrayBuffers: 1.0 MiB
- addedquicIssues and PRs related to the QUIC transport implementation.Issues and PRs related to the QUIC transport implementation.confirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.
on May 26, 2026 This also got backported to
24.16.0.$ volta run --node 24.15.0 node --expose-gc repro.mjs arrayBuffers: 1.0 MiB $ volta run --node 24.16.0 node --expose-gc repro.mjs arrayBuffers: 201.0 MiBProbably related #63290
Will test on Node.js 26.4.0 (or whatever future version with fix)Reacted by Pieter OliverCan confirm, affects multiple versions
Node version Blob.stream()+ read22.22.3 OK 24.15.0 OK 24.16.0 LEAK 24.17.0 LEAK 25.0.0 OK 25.9.0 OK 26.0.0 LEAK 26.2.0 LEAK 26.4.0 LEAK Reacted by Brendan Rosa and Simon HänischReacted by Adam MisrahiAdding one more real-world confirmation because this affects
fetch()uploads with aBlobbody, not only directBlob.prototype.stream()consumers.We saw this in production after moving to Node v24.16.0:
process.memoryUsage().arrayBuffersgrew monotonically whileheapUsedstayed mostly flat, and forced full GC did not reclaim it. The source was an SDK upload path that calls globalfetch()withnew Blob([...])as the request body.Using the same minimal shape as this issue's repro, plus a
fetch()request-body variant, local checks matched the behavior described here:Node v24.14.1: after-GC arrayBuffers ~= 0M Node v24.15.0: after-GC arrayBuffers ~= 0M Node v24.16.0: after-GC arrayBuffers ~= 400M Node v25.9.0: after-GC arrayBuffers ~= 0M Node v26.3.0: after-GC arrayBuffers ~= 400MRequest-body shape on the affected runtime:
Node v24.16.0 + fetch() with Buffer/Uint8Array body: after-GC arrayBuffers ~= 0M Node v24.16.0 + fetch() with Blob body: after-GC arrayBuffers ~= 400MFurther reduction confirms the same core behavior already reported here: direct
Blob.prototype.stream()consumption retains the backing ArrayBuffers, whileBlob.prototype.arrayBuffer()does not. This lines up withReader::wakeup_retaining the stream/reader path.Reacted by Vladislav Botvin and Brendan RosaI can confirm we observed some kind of memory leak since the node 26 release
Hit this in a Next.js app (App Router, standalone output). Same versions as the table: 24.15.0 is fine, 24.16.0 and 26.x leak, and a full GC doesn't get it back.
Maybe worth adding two things as I've been going around the houses trying to figure it out as our next.js ecom store has linear memory increase until OOM multiple times a day.
It's not our code calling .stream() but Next tees force-cache fetch responses (cloneResponse -> body.tee()) and reads the cached copy through Blob.prototype.stream(). So any cached fetch can hit this. It shows up when a request gets aborted: a prefetch kicks off a render that does the cached fetch, the user clicks away, the render is cancelled mid-stream, and the reader is left sitting there with wakeup_ still set.
Second, it leaks way more than the source buffer. Blob::Reader is an AsyncWrap, so wakeup_ also pins the AsyncContextFrame it grabbed when it was created, and everything that frame can reach. For us that's the whole render for that request, not just the 1 MiB buffer. One snapshot on 26.3.0 after a short burst of aborted requests, taken after a full GC: 683 BlobReader, 937 AsyncContextFrame, 928 render objects, 118 MB of arrayBuffers, none of it collected. Retainer path is the same as yours, rooted at Global handles via wakeup_.
On 24.15.0 the same load still piles up under normal GC, but a full mark-compact clears it. 24.16+ and 26.x don't.
- marked Blob.stream()/CompressionStream leak source buffer in RSS on v24.16-v24.18 LTS (cf #63574) #64105 as a duplicate of this issue
on Jun 24, 2026 - changed the title
[-]Blob.prototype.stream() leaks the source buffer on v26[/-][+]Blob.prototype.stream() memory-leaks the source buffer[/+]on Jun 24, 2026 - added a commit that references this issue
on Jul 9, 2026 - added a commit that references this issue
on Jul 21, 2026 - added a commit that references this issue
on Jul 27, 2026 - added a commit that references this issue
on Jul 29, 2026 - marked fetch/FormData multipart uploads retain ArrayBuffer memory after request completion in Node.js 24.16+ #65012 as a duplicate of this issue
on Aug 4, 2026 - added a commit that references this issue
on Aug 6, 2026 - added a commit that references this issue
on Aug 8, 2026 - added a commit that references this issue
on Aug 10, 2026 - added a commit that references this issue
on Aug 14, 2026 - added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Aug 24, 2026 - added a commit that references this issue
on Sep 16, 2026
Version
v26.1.0
Platform
Also reproduces on Linux arm64 (v26.1.0).
Subsystem
buffer
What steps will reproduce the bug?
How often does it reproduce? Is there a required condition?
Every time, as long as
Blob.prototype.stream()is called. The Blob on its own is fine:new Blob([buf])andblob.arrayBuffer()don't retain anything. Only.stream()does, and it doesn't matter whether the stream is cancelled immediately or fully drained.What is the expected behavior? Why is that the expected behavior?
After the loop and a couple of GC passes
arrayBuffersshould be back near baseline (~1 MiB, the single livebuf), because none of the blobs or streams are reachable anymore. That is what v22, v24 and v25 do:What do you see instead?
On v26 every iteration keeps its 1 MiB input buffer alive permanently:
A heap snapshot shows each backing store retained by a
Blobthat is pinned in eternal handles:Additional information
I ran into this chasing ~6 GB RSS in a service that uses isomorphic-git, which deflates git objects with
new Blob([buffer]).stream().pipeThrough(new CompressionStream('deflate')). The leak never shows up in the V8 heap, only inarrayBuffers/ RSS, which made it hard to find. It bisects cleanly to the v25 -> v26 boundary.