Skip to content

Blob.stream()/CompressionStream leak source buffer in RSS on v24.16-v24.18 LTS (cf #63574) #64105

Description

@yuntian3008

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 the arrayBuffers-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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions