Bun.nanoseconds — high-resolution clock for Server-Timing
Accept. The response finalizer introduced in spec 19 already
Review verdict (2026-05-27)
Accept. The response finalizer introduced in spec 19 already mutates outgoing-response headers (X-Request-Id, Set-Cookie flush). Adding Server-Timing: total;dur=<ms> is a two-line addition that turns the existing finalizer code path into the standard browser-Network-tab perf indicator. Cost: ~50 lines of new spec, ~10 lines of code. Value: every patties user sees server-side request duration in their dev tools with no setup.
Scope pins:
- Dev only. Production responses must not carry the header (no perf leak, no overhead).
totalonly. Singletotal;dur=<ms>entry; named per-middleware entries are deferred to a follow-up RFC if real demand surfaces.- Adapter parity. Bun adapter uses
Bun.nanoseconds()(bigint ns); edge adapter usesperformance.now()(double ms). Same precision class; identical header shape. - Don't overwrite. A handler that sets its own
Server-Timingheader wins; the finalizer leaves it alone. Same rule asX-Request-Id.
Out of scope for this RFC:
- Named per-middleware entries. Would require ctx-threading. Re-open under a follow-up RFC if asked.
- Always-on production timing. Separate observability concern; apps that need it should wire their own middleware.
Summary
Bun.nanoseconds() returns elapsed nanoseconds since process start as a bigint. Use it in the same response finalizer that already adds X-Request-Id (spec 19) to emit Server-Timing: total;dur=<ms> on every dev response.
Motivation
Patties dev users currently get no built-in server-timing signal in their browser dev tools. They have to wire middleware that calls performance.now() and sets the header themselves. Spec 19 opened the right code path — the outer composer's response finalizer — for a header that should be on every response. Adding Server-Timing here costs nothing and gives every user a visible perf indicator from day one.
Bun.nanoseconds() is the right primitive on Bun: bigint nanoseconds, no allocation, no Date.now() rounding. On edge adapters, performance.now() is the standards-compliant equivalent and is available in every WHATWG-class runtime.
Proposal
Capture timestamps
In the outer composer (the same module that builds PattiesContext):
// Bun adapter
const ns0 = Bun.nanoseconds()
const res = await runRoute(req, ctx)
const ms = Number(Bun.nanoseconds() - ns0) / 1e6
// Edge adapter
const t0 = performance.now()
const res = await runRoute(req, ctx)
const ms = performance.now() - t0
Set the header in dev
The same finalizer that flushes X-Request-Id checks NODE_ENV:
if (Bun.env.NODE_ENV !== "production") {
if (!res.headers.has("Server-Timing")) {
res.headers.set("Server-Timing", `total;dur=${ms.toFixed(1)}`)
}
}
Coexist with spec 19
The Server-Timing write happens after the X-Request-Id write in the finalizer. Both are pure-additive on disjoint header names.
Trade-offs
.toFixed(1)reveals server timing shape in dev. That's the point — the developer wants to see it. Prod is unaffected by this RFC.- Edge adapter precision.
performance.now()is millisecond-precision-class but actually a double — same effective resolution asBun.nanoseconds()rounded to ms. - No named entries. Conscious scope. Easier to ship now and add later than to ship a wider API we can't easily walk back.
Open questions
- Should we also emit Server-Timing in prod when a user opts in via config? Deferred — open a follow-up RFC if asked.
- Per-middleware named entries (
auth;dur=...,db;dur=...)? Deferred — requires ctx-threading.