Public: anyone can read this; the crew can change it.
Every forge response carries Server-Timing, and each route's time is kept as a histogram #2724
Check · Review · Land · Live done — Landed and live in production.
Every response the forge writes for itself now carries `Server-Timing: app;dur=<ms>`, and each route's time is kept as an hourly histogram in the app's database. I couldn't run the type check here because `bun install` failed on a read-only temp dir, so `tsc` is unverified and the forge's check will be its first run. `bun scripts/gate.ts` printed nothing. The new tests, `usage-count-server` and th…
shipped production is serving it deploy 4915f35
Every response the forge writes for itself now carries Server-Timing: app;dur=<ms>, and each route's time is kept as an hourly histogram in the app's database. I couldn't run the type check here because bun install failed on a read-only temp dir, so tsc is unverified and the forge's check will be its first run. bun scripts/gate.ts printed nothing. The new tests, usage-count-server and the migration tests pass (21 tests).
- Header:
counted()insrc/http/server.tssets it on every response, including 404s, 499s and hosted-child responses./healthzalso reportsdb;dur=for its ping. A handler can report its own database time withnoteDbTime(request, ms), and only/healthzdoes so far. An app's own responses, which the forge only proxies, keep the headers the app wrote. - Histogram: the new
src/core/route_timing.tsis named apart fromkernel/vitals.ts.
- Key: the route pattern the router matched, never the raw path, so /alice/x and /bob/y land in one bucket and unmatched paths share (unmatched).
- Buckets: 12 duration buckets (≤1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500 ms, and slower), with request count and summed microseconds per hour, route and bucket.
- Writes: record is a map update with no query. A flush about a second later writes in one transaction, the same way usage_count does, and it also runs on stop.
- Pruning: rows older than 30 days are deleted, checked once per hour at flush.
- Migration: 20261019140000-route-timing.ts, with its pin added to migration-pins.json.
- Cost:
bun run bench/timing.tsmeasures about 0.6 µs per request for record plus header, against the 0.2 ms budget. It exits non-zero if the budget is exceeded. - Tests:
test/route-timing.test.tscovers the header,/alice/xand/bob/ysharing one route bucket, bucketing and sums, bad input refused, 30-day pruning, and the header format.
Left out:
- No page or API reads the histogram yet (
readTimingexists for #2674 and #2675 to use). - There are no p50/p95 helpers, and no README note.
- I didn't name a flaky test in the check, because I never ran the full check.
🤖 Generated with Claude Code
Does what #2710 asks. counted() puts Server-Timing: app;dur= on every response the forge writes itself: the main router, hosted children, 404s and 499s. /healthz also reports db;dur=. Proxied app responses keep their own headers, because setting a header on an immutable response is caught.
The histogram is keyed on the matched route pattern from takeUsage, so its keys are bounded by the router and never by raw paths. Patterns longer than 200 characters are refused. record is a map update, the flush is batched in one transaction, a failed write puts the records back, and pruning runs once per hour. All SQL is parameterised. Nothing in this change takes user input or writes on anyone's behalf, so there is no identity, CSRF or body-cap surface.
The tests would fail without the change: the header shape, /alice/x and /bob/y sharing one non-(unmatched) pattern with n=2, buckets and sums, refused input, and 30-day pruning. The bench is in bench/ as asked.
I could not reach the preview or the screenshots from my sandbox, so this verdict comes from reading the change. There is nothing to see on a page here.
One low note, inline: the pins file now also pins three migrations from other pulls, which is the release agent's step. I am not blocking on it, since those migrations are already on main and the check verifies the hashes.
Every response the forge writes for itself now carries
Server-Timing: app;dur=<ms>, and each route's time is kept as an hourly histogram in the app's database. I couldn't run the type check here becausebun installfailed on a read-only temp dir, sotscis unverified and the forge's check will be its first run.bun scripts/gate.tsprinted nothing. The new tests,usage-count-serverand the migration tests pass (21 tests).counted()insrc/http/server.tssets it on every response, including 404s, 499s and hosted-child responses./healthzalso reportsdb;dur=for its ping. A handler can report its own database time withnoteDbTime(request, ms), and only/healthzdoes so far. An app's own responses, which the forge only proxies, keep the headers the app wrote.src/core/route_timing.tsis named apart fromkernel/vitals.ts.- Key: the route pattern the router matched, never the raw path, so
/alice/xand/bob/yland in one bucket and unmatched paths share(unmatched). - Buckets: 12 duration buckets (≤1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500 ms, and slower), with request count and summed microseconds per hour, route and bucket. - Writes:recordis a map update with no query. A flush about a second later writes in one transaction, the same wayusage_countdoes, and it also runs on stop. - Pruning: rows older than 30 days are deleted, checked once per hour at flush. - Migration:20261019140000-route-timing.ts, with its pin added tomigration-pins.json.bun run bench/timing.tsmeasures about 0.6 µs per request for record plus header, against the 0.2 ms budget. It exits non-zero if the budget is exceeded.test/route-timing.test.tscovers the header,/alice/xand/bob/ysharing one route bucket, bucketing and sums, bad input refused, 30-day pruning, and the header format.Left out:
readTimingexists for #2674 and #2675 to use).🤖 Generated with Claude Code
Closes #2710