Server-Timing — dev-only response duration header
This spec encodes [[rfc-bun-nanoseconds]]. It extends the response
This spec encodes [[rfc-bun-nanoseconds]]. It extends the response finalizer introduced in [[framework/archive/19-request-id|spec 19]] so that every response in dev carries Server-Timing: total;dur=<ms> alongside X-Request-Id. Production responses are unchanged.
Goal
Give the framework a wall-clock duration that shows up in browser dev-tools without configuration: open the Network panel, look at any response, see how long the server took. No middleware to install, no manual instrumentation, no setup in patties.config.ts.
Surface
No new user-facing API. The framework adds one outgoing header in dev:
Server-Timing: total;dur=<ms>
Where <ms> is the wall-clock duration from the moment the outer composer enters request handling to the moment the response finalizer is about to flush the response, rendered to one decimal place.
Framework behaviour
Timing capture
In the Bun adapter (src/adapters/bun/):
const ns0 = Bun.nanoseconds()
// ...middleware + handler run...
const elapsedNs = Bun.nanoseconds() - ns0
const ms = Number(elapsedNs) / 1e6
In the edge adapter (src/adapters/edge/):
const t0 = performance.now()
// ...middleware + handler run...
const ms = performance.now() - t0
Both produce a double-precision millisecond value; same precision class for the header.
Emission
The same response finalizer that flushes X-Request-Id and Set-Cookie checks NODE_ENV and adds the header:
if (Bun.env.NODE_ENV !== "production") {
if (!res.headers.has("Server-Timing")) {
res.headers.set("Server-Timing", `total;dur=${ms.toFixed(1)}`)
}
}
The detection rides on the same NODE_ENV check the dev error overlay uses. No new config knob.
Header coexistence with spec 19
The Server-Timing write happens after the X-Request-Id write. Both are pure-additive on disjoint header names; neither blocks the other.
A handler that has already set Server-Timing itself wins — the finalizer never overwrites an existing Server-Timing header. Same rule pattern as the X-Request-Id contract.
Implementation note (shipped 91848f2). Timing and emission live in the shared
finalizeResponse(src/middleware/cookies.ts), invoked from the router (src/router/index.ts) for both per-route handlers and the 404 fallback. Capture rides anow()helper that feature-detects the runtime —Number(Bun.nanoseconds()) / 1e6on Bun, elseperformance.now()on edge — andisProduction()readsNODE_ENVfromBun.envorprocess.env. The per-adapter sketch above is collapsed into this one path; the edge shape is covered bytests/adapters/edge.test.ts.
User contract
In dev, every response has a Server-Timing: total;dur=... header. In prod, nothing changes — no header, no overhead.
A handler that wants to add its own entries can do so by setting its own Server-Timing header on the returned Response; the finalizer sees the existing header and skips its own write.
Non-goals
- Named per-middleware entries. Out of scope. Adding
auth;dur=.../db;dur=...would require ctx-threading and actx.timingAPI; if real demand surfaces, open a follow-up RFC. - Production timing. Out of scope. Apps that need prod timing should wire their own observability middleware that reads
Bun.nanoseconds()/performance.now()directly. - Sub-millisecond precision.
.toFixed(1)is the contract; clients that read more decimals will be disappointed. - A dedicated
serverTimingconfig knob. The dev/prod gate is the only switch. If users want to opt out entirely in dev, they can set their ownServer-Timingheader to an empty value in middleware.
Acceptance criteria
- In dev, a request to any route returns a response with a
Server-Timing: total;dur=<X.Y>header where<X.Y>is a positive number rendered to one decimal place. - In prod (
NODE_ENV=production), the same request returns a response with noServer-Timingheader. - A handler that sets its own
Server-Timingheader sees the framework leave it unchanged in both dev and prod. - On the edge adapter, dev mode still produces a
Server-Timingheader usingperformance.now(); the header shape is identical. - The header coexists with
X-Request-Id(spec 19); both appear on the same response with no interference.
Test plan
- Unit: outer composer in dev → assert
Server-Timingheader present and matches^total;dur=\d+\.\d$. - Unit: same with
NODE_ENV=production→ assert header absent. - Unit: handler returning a response with its own
Server-Timingheader → assert finalizer preserves it verbatim. - Adapter parity: edge adapter test asserts same shape via
performance.now(). - Integration: a request also carries
X-Request-Idper spec 19 — assert both headers appear on the same response.
Out of this spec
The dev error overlay (src/render/dev-error-overlay.ts) and the CLI log formatting (cli/archive/07-logging-errors) are unaffected.