Skip to content

CONCEPT Cited by 1 source

BYOB reads (bring-your-own-buffer)

BYOB (bring-your-own-buffer) reads are the Web Streams mechanism for reducing per-chunk allocation in byte-oriented streams: instead of the stream allocating a new buffer for each read and handing it to the consumer, the consumer provides its own pre-allocated buffer and the stream fills it. The intent is zero-copy / buffer-reuse on high-throughput byte streams.

The intended semantics

const reader = stream.getReader({ mode: 'byob' });
const buffer = new ArrayBuffer(1024);
let view = new Uint8Array(buffer);

const result = await reader.read(view);
// 'view' should now be detached and unusable
// result.value is a NEW view, possibly over different memory
view = result.value;  // must reassign

Key mechanics:

  • A separate reader type: ReadableStreamBYOBReader.
  • A separate controller: ReadableByteStreamController.
  • A separate request type: ReadableStreamBYOBRequest.
  • ArrayBuffer detachment: the buffer passed in becomes unusable; the stream returns a new view over potentially different memory.

The complexity surface

The 2026-02-27 Cloudflare post enumerates the BYOB footprint:

  1. Separate API surface — a whole parallel reader type means consumers need to know whether to call getReader() or getReader({ mode: 'byob' }).
  2. Buffer lifecycle management — callers must track detachment and reassign.
  3. Detachment semantics — transferring an ArrayBuffer is error-prone and subtly different from normal JS value semantics.
  4. Not composable with async iterationfor await…of can't access BYOB; consumers who want zero-copy are forced back into the manual reader loop.
  5. Not composable with TransformStream — same gap, from the other side.
  6. Producer complexity — a correct byte-stream implementation must handle both default and BYOB read patterns simultaneously. The post shows the full producer template: controller.byobRequest branch + default controller.enqueue branch, both maintained in parallel.
  7. WPT suite burden — BYOB has its own dedicated edge-case test files: detached buffers, bad views, response-after- enqueue ordering, WebAssembly memory rejection (BYOB reads must explicitly reject ArrayBuffers backed by Wasm memory, which look like regular buffers but can't be transferred).

The "complexity without payoff" critique

The post's argument is that BYOB is rarely used to any measurable benefit despite shipping all the above complexity:

"In practice, (and yes, there are always exceptions to be found) BYOB is rarely used to any measurable benefit. […] Most userland implementations of custom ReadableStream instances do not typically bother with all the ceremony required to correctly implement both default and BYOB read support in a single stream — and for good reason. It's difficult to get right and most of the time consuming code is typically going to fallback on the default read path."

The cost lands on both sides:

  • Consumers rarely reach for BYOB because the ergonomics (explicit reader type, detachment reassignment, no for await…of) are worse than default reads.
  • Producers rarely implement BYOB correctly because getting both paths right is a double implementation burden.
  • Implementers pay the full spec complexity — tracking pending BYOB requests, handling partial fills, coordinating between the BYOB reader and the underlying source.
  • Runtimes can only realize the zero-copy benefit for host-provided streams (fetch response body) where the runtime controls both ends.

The implicit conclusion: a byte-oriented streaming API should either be default zero-copy (never require separate ceremony to skip allocation), or treat chunks opaquely and not expose partial-fill / buffer-reuse at all. systems/new-streams takes the latter position:

"While the API uses Uint8Array, it treats chunks as opaque. There is no partial consumption, no BYOB patterns, no byte-level operations within the streaming machinery itself. Chunks go in, chunks come out, unchanged unless a transform explicitly modifies them."

Alternative views

Host-provided streams (runtime-produced bodies from fetch, file reads, etc.) can benefit from BYOB when the runtime implements it natively — the runtime knows both the consumer buffer and the underlying byte source, can genuinely zero-copy. Some production paths in Node.js / Deno / Bun / Workers do use BYOB internally even when exposed through a default-read surface. The critique is specifically about the user-facing API mandating the BYOB surface.

Seen in

Last updated · 200 distilled / 1,178 read