Skip to content

IMPORTANT: Bun listener per-request overhead (Request clone + zlib bridge) erodes the win #756

Description

@vivek7405

Problem

The Bun listener shell adds per-request overhead that erodes the advertised "~1.9x more req/s" win (which is honestly scoped to the listening path only, not end-to-end throughput). Two hot-path costs on every request:

  1. A full new Request(req, { headers }) clone on every request to stamp the remote IP (stampRemoteIp).
  2. A web -> node -> web zlib bridge for every compressed response (Readable.from(webStreamChunks(...)) -> compressor -> Readable.toWeb).

For a real SSR page the render cost dominates, so the listener delta is small in practice, but these per-request allocations/conversions are pure overhead on the Bun path specifically. The "~1.9x" figure is also easy to over-read as "webjs is ~2x faster on Bun" end to end. From an architecture audit.

Design / approach

Reduce per-request work on the Bun listener and make the perf claim honest:

  1. Avoid the full Request clone for IP stamping. Investigate carrying the remote IP out-of-band (Bun exposes server.requestIP(req) already used elsewhere) instead of cloning the whole Request. Options: stash the IP on a WeakMap<Request, ip> the context reader consults, or thread it through the ListenerContext rather than reconstructing a Request. The goal is zero Request reallocation on the hot path.
  2. Streamline compression. The shared createCompressor uses node:zlib (native on Bun) which is correct for parity, but the web<->node stream bridging per response is avoidable work for the common small-buffered-body case. Investigate compressing a buffered string/bytes body directly (no stream bridge) and reserving the stream bridge for genuinely streamed bodies.
  3. Publish an end-to-end benchmark (full SSR page with a realistic dependency graph), not just the listening-path microbenchmark, and update the docs so "~1.9x on the listening path" is not mistaken for end-to-end. This directly addresses the audit's "easy to over-read" concern.

Parity is non-negotiable: any change must keep Node and Bun behaviourally identical (the cross-runtime test requirement) and not regress the node:http path.

Implementation notes (for the implementing agent)

  • Where to edit: packages/server/src/listener-bun.js. startBunListener(ctx) hands handle(req) to Bun.serve({ fetch }) (L96-L98). stampRemoteIp(req, srv) at L297-L303 does return new Request(req, { headers }) (the full clone). maybeCompress(resp, req) at L320-L343 builds the web->node->web bridge (Readable.from(webStreamChunks(resp.body)) at L339; comments at L312-L313 / L333-L338 explain why Readable.fromWeb is avoided for mid-stream error propagation).
  • The "~1.9x" claim text is in the file header at L7 ("measured ~1.9x more req/s, which is what justifies"). Update the doc framing (and packages/server/AGENTS.md listener-bun.js row + agent-docs/runtime.md) to scope it to the listening path explicitly.
  • Shared compression seam: packages/server/src/listener-core.js (negotiateEncoding, createCompressor, varyWithAcceptEncoding, isCompressible). Both shells must stay identical (Bun listener: use node:zlib for brotli compression parity #517 brotli/gzip/deflate parity), so a buffered-body fast path should live in the shared core, not just the Bun shell, OR be proven not to diverge.
  • IP plumbing: stampRemoteIp exists because downstream code reads the remote IP from the Request; find the reader (forwarded.js / context.js) and confirm a WeakMap/context-threaded IP reaches it without the clone. Bun's server.requestIP is already used for proxy-IP per the AGENTS.md notes.
  • Landmine (Bun parity is part of the task, AGENTS.md code-workflow item 1): any change to the listener / request path MUST ship a test/bun/*.mjs cross-runtime assertion and be run under node scripts/run-bun-tests.js. Set WEBJS_BUN_VERIFIED=1 only after actually verifying on Bun.
  • Landmine: the compression mid-stream error semantics (why Readable.from not Readable.fromWeb, L333-L338) must be preserved for streamed bodies; the fast path applies only to buffered bodies that cannot error mid-stream.
  • Landmine: 103 Early Hints are node-only (Bun.serve has no informational-response API); do not attempt to add them to the Bun path.

Acceptance criteria

  • The Bun hot path no longer reallocates a full Request per request for IP stamping (IP reaches the reader out-of-band); a test asserts the remote IP is still correct.
  • A buffered-body response is compressed without the per-response web<->node stream bridge (the bridge remains only for streamed bodies); brotli/gzip/deflate output stays byte-correct.
  • Node and Bun remain behaviourally identical; a test/bun/*.mjs cross-runtime test covers the IP + compression paths and is verified on Bun.
  • An end-to-end (full SSR page) benchmark is published alongside the listening-path number.
  • Docs scope the "~1.9x" claim to the listening path (listener-bun.js header, packages/server/AGENTS.md, agent-docs/runtime.md).
  • Tests under packages/server/test/ + test/bun/.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions