Bun.randomUUIDv7 — time-ordered request IDs in middleware
Accept. Closes the "where does the request id come from?" gap that spec
Review verdict (2026-05-27)
Accept. Closes the "where does the request id come from?" gap that spec 10 already implicitly opened by typing requestId: string on AiContext and reaching for crypto.randomUUID(). The framework should generate the id once per request at the outermost middleware boundary and expose it on PattiesContext; downstream consumers (AiContext, log lines, plugin middlewares, the dev-error overlay) read it instead of minting their own.
Scope pins:
- Single source of truth.
ctx.requestIdis generated by the framework in the outer composer (the same place that constructsPattiesContext), before the user middleware runs. User middleware and plugins MUST NOT re-generate it. - Inbound override. If the request carries an
X-Request-Idheader that matches^[A-Za-z0-9._-]{8,128}$, that value is adopted verbatim andBun.randomUUIDv7()is not called. This keeps trace ids stable across an upstream proxy / gateway. Header values that don't match the shape are ignored (not echoed back) — generate fresh instead. - Outbound header. The response finalizer (same place that flushes
Set-Cookie) setsX-Request-Id: <ctx.requestId>on every response, including the 404 fallback and error responses. Already-set values on the user'sResponsewin — we do not overwrite. - Non-Bun targets. Adapters under
src/adapters/that target a runtime withoutBun.randomUUIDv7fall through tocrypto.randomUUID()(UUIDv4). The id is still a string of the same shape; the time-ordering property is best-effort. - AiContext.
createAiContextno longer mints its own id — it accepts the framework'srequestIdfromPattiesContext. Thecrypto.randomUUID()call in spec 10 is replaced by a read ofctx.requestId.
Out of scope for this RFC:
- W3C
traceparent/ OpenTelemetry propagation. Separate concern; the request id is a single opaque string, not a trace context. Revisit under a future observability RFC. - Signing or HMAC'ing the id. The id is informational, not a security primitive.
Summary
Bun.randomUUIDv7() returns a UUID v7: time-ordered (millisecond-precision prefix) and lexicographically sortable by issuance. Patties should generate one per request at the framework boundary and surface it as ctx.requestId on PattiesContext, with an X-Request-Id echo on the response.
Motivation
Spec 07 (framework/phase-1/07-middleware) defines PattiesContext but gives users no way to correlate a request across middleware, handler, log lines, and (when present) the AI subsystem. Spec 10 (framework/phase-3/10-agents-and-tools) already needs a requestId on AiContext and currently reaches for crypto.randomUUID() inline — a local fix that leaves the framework's main request lifecycle without an id of its own. The two surfaces drift.
UUID v7 is the right shape:
- Sortable.
ORDER BY request_idin a log table = chronological order without a separate timestamp column. - Time-encoded prefix. Eyeballing a log file you can see which requests came first.
- Still a string. No new type for users to learn; no breaking change to
AiContext.requestId: string.
Proposal
07-middleware
Extend PattiesContext:
export interface PattiesContext {
// ...existing fields
requestId: string // populated once per request by the framework
}
The outer composer (the place that today builds ctx.url, ctx.cookies, etc. before invoking user middleware) populates ctx.requestId:
const inbound = req.headers.get("x-request-id")
const requestId = inbound && /^[A-Za-z0-9._-]{8,128}$/.test(inbound)
? inbound
: Bun.randomUUIDv7()
The response finalizer sets X-Request-Id: <requestId> on the outgoing Response if the user/handler didn't set it themselves.
User middleware example (replacing the existing logging snippet):
export default defineMiddleware(async (req, ctx, next) => {
const start = performance.now()
const res = await next()
console.log(ctx.requestId, req.method, ctx.url.pathname, res.status,
`${(performance.now() - start) | 0}ms`)
return res
})
10-agents-and-tools
createAiContext accepts the framework's requestId rather than generating one. The example in spec 10 changes from:
const ctx = createAiContext({ requestId: crypto.randomUUID() })
to:
const ctx = createAiContext({ requestId: pctx.requestId })
AiContext.requestId: string is unchanged on the type level.
13-conventions
Note that all framework-generated ids use Bun.randomUUIDv7() so they're sortable in log output. Non-Bun adapters fall through to crypto.randomUUID(); this is documented but not a guarantee.
Adapters
src/adapters/bun/ uses Bun.randomUUIDv7() directly. src/adapters/edge/ uses crypto.randomUUID() (UUIDv4) — the W3C-edge platforms don't expose a v7 helper yet. The framework core depends only on the PattiesContext.requestId: string field, not on the v7-ness, so the adapter swap is local.
Trade-offs
- One more field on
PattiesContext. The spec is mildly heavier; worth it because today every team writes the same five lines of middleware to mint and log a correlation id. - Inbound header trust. Adopting
X-Request-Idverbatim means a malicious client can pin a known id and make their requests appear alongside someone else's in logs. Acceptable for an informational field; the regex stops the obviously-bad shapes (control chars, excessive length). Production deployments that don't want this can strip the header at the edge. - UUIDv4 fallback on non-Bun. Time-ordering is best-effort across targets. Code that relies on sortable ids (e.g. an admin log view) must tolerate v4 mixed with v7. Both sort lexicographically as strings; v4s simply scatter.
Open questions
- Should
ctx.requestIdbe readable insideapp/server.tsboot code or only at request scope? Decision: request-scope only (boot has no request). Boot code that needs an id for its own logging mints its own viaBun.randomUUIDv7(). - Trim header allowlist regex once we have a real corpus of upstream proxies in use. The current
[A-Za-z0-9._-]{8,128}accepts common trace-id encodings (hex, base32, base64url-without-padding) but not bare UUIDs with:separators. Revisit if a real deployment trips it.