CONCEPT Cited by 2 sources
Stream adapter overhead¶
Definition¶
Stream adapter overhead is the allocation / copy / buffering
cost of translating between two streaming API conventions — most
commonly Node.js streams (stream.Readable) and
Web Streams (ReadableStream) —
when a piece of code that wants the one runs on a runtime that
prefers the other.
The cost has three components:
- Buffer duplication. Each streaming implementation has its own internal queue; bytes flowing through both land in two queues in series.
- Allocation churn. Adapters typically wrap each emitted chunk in a small container object / callback closure per hop, creating GC pressure proportional to the chunk rate.
- Chunk-boundary thrashing. Default value-oriented
ReadableStreams usehighWaterMark: 1— each enqueued value is a separate read, even if the values are bytes that could be coalesced.
Canonical Node ⇆ Web double-buffer¶
The idiomatic-looking adapter:
is layered as follows:
res.getBody()—Buffer.concat(chunks)into a single buffer (one copy, one allocation).Readable.from(bufferIterable)— wraps the buffer as a Node.js stream with its own internal ring buffer.Readable.toWeb(nodeReadable)— adapts back into a WebReadableStream, which has its own internal buffer.
Bytes flow through Node's stream buffer and Web Streams' buffer, plus the initial concat copy.
The OpenNext fix (surfaced by Cloudflare's 2025-10 profiling):
returns a ReadableStream directly from the accumulated chunks
without additional copies, extraneous buffering, or passing
everything through inefficient adaptation layers.
Value-oriented vs byte-oriented streams¶
new ReadableStream({ pull() { … } }) without an explicit type
is value-oriented. controller.enqueue(buffer) enqueues the
whole Buffer / Uint8Array as a single value, and the default
highWaterMark is 1 — meaning each enqueue becomes a
separate read, regardless of byte count.
In the 2025-10 Cloudflare profiling of React / Next.js, this was real: byte data flowing through value-oriented streams became thousands of tiny reads where a few would have sufficed.
The fix is an explicit type and a reasonable high-water mark:
const readable = new ReadableStream({
type: 'bytes',
pull(controller) {
controller.enqueue(chunks.shift());
if (chunks.length === 0) {
controller.close();
}
},
}, { highWaterMark: 4096 });
Byte-oriented streams coalesce internally up to highWaterMark
bytes, so consumers pull coalesced 4 KiB blocks instead of
individual small chunks.
Why it compounds on hot paths¶
HTTP response rendering is a concepts/hot-path at CDN / SSR scale. Each chunk / each value that passes through a stream adapter runs once per response, and large-response benchmarks (the 2025-10 case is a 5 MB response) multiply the per-chunk cost.
Cloudflare observed 10-25 % of request processing time in GC on the Next.js / OpenNext benchmark — a disproportionate share driven by adapter-induced allocation churn (plus other sources profiled separately). Removing the adapter layer or flipping to byte-oriented streams cut much of it.
Mitigations¶
- Skip the adapter when you already have the shape you want.
If bytes-in-hand,
ReadableStream.from(chunks)instead ofReadable.toWeb(Readable.from(...)). - Use byte-oriented
ReadableStreams with a sensiblehighWaterMark(e.g. 4096) for byte data. - Respect back-pressure end-to-end. Cloudflare notes that parts of the Next.js / React rendering pipeline don't use byte streams or back-pressure signals well; fixes in the platform layer can only recover some of that.
- Pay attention to the interface between ecosystems. The cost pattern is Node ⇆ Web here, but generalizes to any two streaming APIs with their own buffering assumptions (gRPC streaming ⇆ Kafka, Kafka ⇆ RocksDB iterators, etc).
Sibling concept¶
concepts/head-of-line-buffering is the same shape at the
HTTP-proxy / middleware layer — streaming-hostile defaults
silently collapse chunked streams back into blobs (nginx
proxy_buffering, compression middleware, objectMode
transforms). Stream adapter overhead is the in-process version
of the same pathology.
Seen in¶
- sources/2025-10-14-cloudflare-unpacking-cloudflare-workers-cpu-performance-benchmarks
— canonical wiki instance:
Readable.toWeb(Readable.from(…))double-buffering in OpenNext; value-oriented vs byte-orientedReadableStreampitfall in React / Next.js;ReadableStream.from(chunks)as the adapter-skip fix. - sources/2026-02-27-cloudflare-a-better-streams-api-is-possible-for-javascript
— generalizes the adapter-surface critique: the root cause is
two streaming APIs with incompatible buffering / evaluation
models (Node
stream.Readablepush + internal queue vs WebReadableStreampush + internal queue). Snell's POC new-streams usesAsyncIterableas the lingua franca, bridging to Web streams in ~5 lines without intermediate buffering.
Related¶
- systems/web-streams-api — the streaming API substrate.
- systems/new-streams — the POC alternative that uses async iteration as the cross-API adapter lingua franca.
- systems/opennext — the OpenNext Cloudflare adapter is the specific consumer of the patterns here.
- concepts/async-iteration — the
for await…ofprotocol that bridges push-based producers into pull-based consumers. - concepts/promise-allocation-overhead — the cost amplified by the double-buffering.
- concepts/streaming-ssr — the upstream use case generating these streams.
- concepts/head-of-line-buffering — sibling streaming-hostile-defaults pattern at the HTTP proxy layer.
- concepts/hot-path — why per-chunk adapter cost compounds.