SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

0 0 150 Rust

Bun.serve: restore per-request GC memory accounting to fix elevated RSS under HTTP load (#31422)

### What

Peak RSS of `Bun.serve` hello-world servers under HTTP load regressed vs
v1.3.14 (last Zig-based release). Measured with `oha -c 125 -z 10s`
against `Bun.serve({ fetch: () => new Response("Hello, World!") })` and
an Elysia hello-world, reading `VmHWM` from `/proc/<pid>/status` (Linux
x64, 16 cores), across official and locally-built release binaries:

| server | v1.3.14 | main (unfixed) | main + this PR |
|---|---|---|---|
| `Bun.serve` | 46 MB | 53–61 MB | **42–43 MB** |
| Elysia | 70 MB | 78–83 MB | **65–67 MB** |

(The same regression shows up as 45→58 MB / 77→123 MB on a faster
benchmark machine.) It reproduces on plain canary builds, so it is
unrelated to the fat-LTO work, and it is already present before the
recent Blob content-type changes.

It is **not a leak**: RSS is flat over 5 minutes / 45M requests and
returns to baseline when the load stops. It is a higher steady-state
plateau.

### Cause

Every request creates native state that is only reclaimed when the GC
collects its JS wrappers: the `Request`/`Response` boxes, the
`AbortSignal`, the body string. The Zig server accounts for this by
reporting `@sizeOf(Ctx)` of extra memory to the GC for every claimed
request context (`server.zig:2490`), which keeps eden collections
frequent under sustained load so per-request garbage is reclaimed
promptly.

The Rust port originally preserved that call, but #30909 removed it.
With no allocation pressure reported, JSC collects far less often under
load — the JS heap itself stays tiny, but a rolling backlog of
dead-but-uncollected request garbage sits in the native allocators and
raises the plateau by ~15–20 MB.

Evidence: under load, `process.memoryUsage().external` (JSC's
extra-memory accounting) sits at ~15.7 MB on v1.3.14 vs ~55 KB on main;
forcing more frequent collections on an **unmodified** canary
(`Bun.gc(false)` every 4096 requests) brings the plateau right back to
v1.3.14 levels (65 MB → 49 MB), confirming the mechanism.

### Fix

Bring the per-request report back, exactly as the Zig server does it:
report `size_of::<Ctx>()` to the GC for every claimed request context,
in both `prepare_js_request_context` (HTTP/1) and the generic
`prepare_js_request_context_for` (H3) path.

### Verification

- Release benchmark above: peak RSS back below the v1.3.14 baseline with
this patch applied.
- Throughput: unchanged within run-to-run noise (±3%) on config-matched
local release builds (unfixed ~165k req/s vs fixed 160–168k req/s across
sessions on the same box); v1.3.14 performs this exact per-request
report and sustains the same throughput. (Official canary binaries
aren't directly comparable to local builds on req/s because the release
build configuration just changed.)
- Debug-build A/B driving 16,384 requests at the debug server from a
release-build client: 127 req/s both with and without the change — no
added cost in debug/ASAN lanes.
- New test `test/js/bun/http/serve-request-extra-memory.test.ts`: spawns
a hello-world server, drives 2,000 keep-alive requests, and asserts the
server's `process.memoryUsage().external` grows by more than 256 KiB (it
grows by ~sizeof(RequestContext) per request with the fix, and stays
within ~9 KB of noise without it). Fails on an unfixed build, passes
with this PR.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
R
robobun committed
19dd34df338f66648ff984d2754ec6f0ff3ff76f
Parent: cab4feb
Committed by GitHub <noreply@github.com> on 5/26/2026, 10:23:14 AM