You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Blob.prototype.stream() memory-leaks the source buffer #63574
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]) and blob.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 arrayBuffers should be back near baseline (~1 MiB, the single live buf), because none of the blobs or streams are reachable anymore. That is what v22, v24 and v25 do:
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 in arrayBuffers / RSS, which made it hard to find. It bisects cleanly to the v25 -> v26 boundary.
Adding one more real-world confirmation because this affects fetch() uploads with a Blob body, not only direct Blob.prototype.stream() consumers.
We saw this in production after moving to Node v24.16.0: process.memoryUsage().arrayBuffers grew monotonically while heapUsed stayed mostly flat, and forced full GC did not reclaim it. The source was an SDK upload path that calls global fetch() with new 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:
Further reduction confirms the same core behavior already reported here: direct Blob.prototype.stream() consumption retains the backing ArrayBuffers, while Blob.prototype.arrayBuffer() does not. This lines up with Reader::wakeup_ retaining the stream/reader path.
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.
changed the title [-]Blob.prototype.stream() leaks the source buffer on v26[/-][+]Blob.prototype.stream() memory-leaks the source buffer[/+]on Jun 24, 2026
Activity
Renegade334 commentedon May 26, 2026
Reader::wakeup_is keeping the stream reference alive (cc @jasnell)fahrradflucht commentedon May 27, 2026
This also got backported to
24.16.0.darky commentedon May 28, 2026
Probably related #63290
Will test on Node.js 26.4.0 (or whatever future version with fix)
de-tester commentedon May 30, 2026
Can confirm, affects multiple versions
Blob.stream()+ readjayyao97 commentedon Jun 4, 2026
Adding 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:Request-body shape on the affected runtime:
Further 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.alexisclarembeau-ctrl commentedon Jun 5, 2026
I can confirm we observed some kind of memory leak since the node 26 release
makit commentedon Jun 16, 2026
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.
[-]Blob.prototype.stream() leaks the source buffer on v26[/-][+]Blob.prototype.stream() memory-leaks the source buffer[/+]buffer: fix Blob.stream() leaking source buffer
buffer: fix Blob.stream() leaking source buffer
fix: drop multipart upload path; ingest with format=pprof (#316)
Revert to Node 22 (#2057)
buffer: fix Blob.stream() leaking source buffer
buffer: fix Blob.stream() leaking source buffer
fix(gateway): stop retaining every payload we gzip (#442)
chore(deps): bump @pyroscope/nodejs 0.6.1 -> 0.6.2 (#3778)
Fix postFormAsync memory leak
fix(node): send gzip bytes instead of a Blob request body
fix(node): send gzip bytes instead of a Blob request body (#4614)
revert: hand-built multipart machine learning requests