Repository navigation
Blob.stream()/CompressionStream leak source buffer in RSS on v24.16-v24.18 LTS (cf #63574) #64105
Description
Version
v24.16.0, v24.17.0, and v24.18.0 (entire current 24.x line). Confirmed clean on v24.15.0 and on v22.20.0 / v22.21.0.
Platform
Reproduced on Linux under Docker on both musl (node:<version>-alpine) and glibc (debian:bookworm-slim + the official nodejs.org build), and on both arm64 and x64. The leak is libc- and arch-independent.
Subsystem
stream / webstreams / Blob
What steps will reproduce the bug?
Consuming a native-backed ReadableStream — the output of Blob.prototype.stream() or CompressionStream — by draining it (e.g. new Response(stream).arrayBuffer()) leaks the native source buffer on every call. The growth is in RSS (off-heap / native); it is not visible in arrayBuffers or the V8 heap.
Minimal reproducer (no dependencies) — save as repro.js:
const buf = Buffer.alloc(50_000);
let i = 0;
setInterval(() => {
console.log(i, "rssMB", (process.memoryUsage().rss / 1048576).toFixed(0));
}, 5000);
(async () => {
for (;;) {
await new Response(new Blob([buf]).stream()).arrayBuffer();
i++;
}
})();Run:
docker run --rm --memory=4g -v "$PWD":/app -w /app node:24.17.0-alpine node repro.js
How often does it reproduce? Is there a required condition?
Every run, immediately. It requires a native-backed stream source (Blob.stream() or CompressionStream); a hand-written JS ReadableStream consumed the same way does not leak.
What is the expected behavior? Why is that the expected behavior?
RSS stays flat — each iteration's Blob/stream is unreachable after arrayBuffer() resolves, so the native source buffer should be released.
What do you see instead?
RSS grows without bound. Measured RSS in a tight drain loop (50 KB payload, --memory=4g):
| Node | RSS | |
|---|---|---|
| v24.15.0 | ~208 MB | flat |
| v24.16.0 | ~4 GB (≈4 GB within 10 s) | leak |
| v24.17.0 | ~4 GB (≈4 GB within 10 s) | leak |
| v24.18.0 | ~4 GB (≈4 GB within 10 s) | leak |
| v22.20.0 / v22.21.0 | ~130 MB | flat |
The v24.18.0 row was measured on glibc/arm64 (debian:bookworm-slim, official build): RSS hit ~4 GB in ~10 s and the process was OOM-killed at ~110 k iterations, whereas v22.21.0 on the identical base ran the full window flat at ~127 MB across ~4.1 M iterations. The leak is still present in the latest 24.x (v24.18.0) — it has not been fixed on the 24.x line.
CompressionStream output (also native-backed) behaves identically. A plain JS ReadableStream consumed via the same new Response(stream).arrayBuffer() does not leak — so the leak is specific to the native source buffer, not to Response/stream consumption in general.
Additional information
This is very likely related to #63574 (“Blob.prototype.stream() leaks the source buffer on v26”, open, fix PR #63577) and #63708 (v24.16.0, closed as a duplicate of #63574). The distinction motivating this report:
- Blob.prototype.stream() memory-leaks the source buffer #63574's bisect uses
arrayBuffers+stream.cancel()and lands on v26 only (v24.15.0 reported clean). - The drain path (
new Response(stream).arrayBuffer()), measured by RSS, already regresses across the v24.16.0 → v24.18.0 LTS line — which thearrayBuffers-based bisect does not capture.
Real-world impact: a service logging via @axiomhq/pino (which gzips each batch with new Blob([data]).stream().pipeThrough(new CompressionStream('gzip'))) leaked ~140–230 MB/day of off-heap RSS in production on Node 24.16 and eventually exhausted host RAM; pinning the runtime to Node 22 resolved it. Filing because the LTS / RSS manifestation isn't captured by #63574's current bisect and may need a backport to 24.x — happy to consolidate if maintainers consider it the same root cause.
Activity
[-]Blob.stream()/CompressionStream leak source buffer in RSS on v24.16-v24.17 LTS (cf #63574)[/-][+]Blob.stream()/CompressionStream leak source buffer in RSS on v24.16-v24.18 LTS (cf #63574)[/+]fix: avoid Blob upload body to prevent native ArrayBuffer leak
gabrielbryk commentedon Jul 21, 2026
Opened a v24.x backport: #64664.
One wrinkle worth flagging: the JS fix calls
reader.setWakeup(undefined),but v24.x's C++
Blob::Reader::SetWakeuppredates the guard that acceptsundefined(it shipped in the QUIC commit #62876, not on this line), so aplain cherry-pick builds but aborts at runtime with
args[0]->IsFunction().The PR carries a second commit with just that four-line
node_blob.ccguard;the QUIC-specific parts of #62876 are omitted.
parallel/test-blob*(incl.the new
test-blob-stream-gc.js) passes on a local build.chore: revert memory leak investigation
fix(gateway): stop retaining every payload we gzip (#442)