SYSTEM Cited by 2 sources
Web Streams API¶
Web Streams (MDN)
is the WHATWG Streams Standard
— ReadableStream, WritableStream, TransformStream. Designed
2014-2016 as a common cross-runtime streaming API, now the
substrate under fetch() bodies in browsers, Deno, Bun, and
Cloudflare Workers.
Node.js has it too (via Readable.toWeb /
Writable.toWeb) but most older Node code uses Node's own
earlier stream.Readable / stream.Writable API.
Stream types¶
Two flavours of ReadableStream:
- Value-oriented (default): enqueued items are arbitrary JS
values. Default
highWaterMark: 1— one enqueue is one read. This is what you get withnew ReadableStream({ pull() { … } })without an explicittype. - Byte-oriented:
type: 'bytes'. The controller can coalesce enqueuedUint8Array/Bufferchunks internally; reads pull up tohighWaterMarkbytes across however many underlying chunks satisfy the request.
The 2025-10 Cloudflare-benchmark post flagged React and Next.js
as passing bytes through value-oriented streams with default
highWaterMark: 1 — so a stream of 1000 × 1-byte chunks became
1000 round trips into the stream instead of one ~4 KiB coalesced
read. Explicit fix: declare type: 'bytes' + a reasonable
highWaterMark (e.g. 4096).
The 2026-02-27 structural critique¶
James Snell — Cloudflare Workers runtime engineer and Node.js TSC member — published a structural critique arguing Web streams' usability and performance issues cannot be fixed with incremental improvements; they are consequences of foundational design choices made 2014-2016, before async iteration landed in ES2018. See sources/2026-02-27-cloudflare-a-better-streams-api-is-possible-for-javascript for the full treatment. Key points:
Excessive ceremony for common operations¶
Reading a stream to completion the spec way:
const reader = stream.getReader();
const chunks = [];
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(value);
}
} finally {
reader.releaseLock();
}
— vs the same thing through the async-iteration retrofit:
The async-iteration form is better, but the underlying complexity (readers, locks, controllers) is still there hidden — and BYOB reads can't be accessed through iteration, so zero-copy consumers must fall back to the manual reader loop.
Locks are footguns¶
getReader() acquires an exclusive lock; forgetting
releaseLock() permanently breaks the stream. The locked
property tells you that a stream is locked, not why, by
whom, or whether the lock is even still usable. Piping
internally acquires locks, making streams silently unusable
during pipeTo() in ways that aren't obvious.
BYOB ships complexity without payoff¶
BYOB reads require a separate reader
type, separate controller, ArrayBuffer detachment semantics,
and dedicated WPT test files for edge cases (detached buffers,
bad views, WebAssembly memory rejection). Most userland
ReadableStream implementations don't bother with the dual
default/BYOB path; most consumers stick with default reads;
BYOB can't compose with async iteration or TransformStream.
Backpressure is advisory-only¶
controller.desiredSize exposes a signal, but
controller.enqueue() always succeeds regardless — producers
can and do ignore it. writer.ready exists on the writable
side but is routinely ignored. tee() branches buffer
unboundedly when one side reads faster than the other;
the spec mandates no buffer limit. See concepts/backpressure
for the structural discussion.
Per-operation promise allocation¶
Every read() creates a promise; internally, spec-mandated
coordination promises exist for queue management, pull()
coordination, backpressure signaling. At streaming SSR scale
(hot paths running per-request),
short-lived allocation churn lands as 10-25 % of request CPU
in GC, per the 2025-10 measurement — up to 50 %+ per the
2026-02 post. See concepts/promise-allocation-overhead.
Unconsumed bodies leak connections¶
If you call fetch(url) and only check response.ok without
consuming or cancelling the body, the stream holds a reference
to the underlying connection until GC runs. This has caused
connection pool exhaustion in Node's undici. Request.clone()
and Response.clone() implicitly tee(), compounding this.
TransformStream is push-based¶
transform() runs on write, not on read. Synchronous
transforms that always controller.enqueue() never signal
back-pressure to the writable side, even when the readable
side's buffer is full. A 3-transform chain can fill six
internal buffers simultaneously before the consumer starts
reading. See concepts/pull-vs-push-streams.
Optimization treadmill is unsustainable¶
Every major runtime has invented non-standard internal escape hatches to make Web streams fast:
| Runtime | Escape hatch |
|---|---|
| Node.js | Vercel's proposed fast-webstreams (10× promise elision) |
| Deno | Native-path optimizations |
| Bun | "Direct Streams" (deliberately non-standard API) |
| Workers | IdentityTransformStream (Workers-specific) |
Each "works in some scenarios but not in others, in some runtimes but not others […] creates friction for developers trying to write cross-runtime code, particularly those maintaining frameworks that must be able to run efficiently across many runtime environments."
Node streams vs Web streams¶
Workers prefer Web Streams. Node.js frameworks built against
stream.Readable need an adapter to produce a ReadableStream.
The straightforward-looking adapter chain:
runs data through two buffers — Node's stream internal buffer
and the Web stream's internal buffer. For bytes-in-hand use
cases, ReadableStream.from(chunks) skips both adapter layers.
See concepts/stream-adapter-overhead.
Third-party corroboration from Vercel:
"Native WebStream pipeThrough at 630 MB/s for 1KB chunks.
Node.js pipeline() with the same passthrough transform:
~7,900 MB/s. That is a 12× gap, and the difference is almost
entirely Promise and object allocation overhead."
(Malte Ubl, We Ralph Wiggum'd WebStreams)
Proposed alternative: new-streams¶
systems/new-streams is Snell's 2026-02-27 proof-of-concept alternative built around AsyncIterable + pull-based + explicit backpressure policies. Benchmarks 2×-120× faster than Web streams across every tested runtime; not a ship-it proposal, explicitly a conversation-starter about what JS streaming "should" look like post-ES2018.
Integration with streaming SSR¶
Web Streams is the substrate under React 18 streaming SSR
(renderToReadableStream) — see concepts/streaming-ssr.
Pitfalls around buffering
defaults at intermediate hops (compression middleware, proxies)
apply here; the 2025-10 Cloudflare post adds the stream-adapter
layer as another place collapse happens invisibly. The 2026-02
post argues that the per-request GC cost of streaming SSR
through Web streams can reach 50 %+ of CPU time per request —
making the current substrate a primary optimization target
for server-side rendering frameworks.
Seen in¶
- sources/2025-10-14-cloudflare-unpacking-cloudflare-workers-cpu-performance-benchmarks
— canonical wiki instance of value-oriented vs byte-oriented
ReadableStreamin React / Next.js and of Node-stream ⇆ Web- stream adapter double-buffering in the OpenNext Cloudflare stack. - sources/2026-02-27-cloudflare-a-better-streams-api-is-possible-for-javascript
— canonical wiki structural critique enumerating Web streams'
design-level issues; source of the
new-streamsalternative proposal.
Related¶
- systems/cloudflare-workers — Workers' native streaming API.
- systems/nodejs — parallel streaming APIs (Node streams +
Web streams) via
toWeb. - systems/new-streams — the POC alternative.
- systems/opennext — the Next.js → Workers adapter where Node ⇆ Web translations happen.
- concepts/stream-adapter-overhead — canonical description of the Node ⇆ Web stream cost surface.
- concepts/backpressure — the control primitive Web streams makes advisory.
- concepts/async-iteration — the ES2018 primitive Web streams was designed before.
- concepts/pull-vs-push-streams — the axis Web streams is push-biased on.
- concepts/promise-allocation-overhead — the per-operation cost that dominates Web streams performance.
- concepts/byob-reads — the zero-copy-but-complex reader surface.
- concepts/streaming-ssr — the consumer substrate.
- concepts/head-of-line-buffering — sibling "streaming-hostile default" pattern at the HTTP-proxy tier.