Skip to content
Ali Akbari
Menu

ADR-0021 · accepted · 13 October 2026

Pages read the visitor's own request back to them

Context

The site measures itself (ADR-0016, ADR-0017), but a visitor only meets that work as a page that describes it. The most direct demonstration is the visitor's own request: how long it took, where the time went, and what the measurement kept of it. That must not collect anything new, add an endpoint to a single host, or hand an attacker information that helps them tune requests.

Options

  • A per-request lookup endpoint ("find my record"). Exact, but it needs a request identifier in the ledger, which makes records linkable and breaks the property ADR-0016 publishes, and it is a new target on a single host.
  • Fetch timing from the API after load. No new data, but another request per page view, and the page would depend on the API (ADR-0003).
  • Reflect nginx's timings in a Server-Timing header on the response the visitor already receives, and let the page combine it with the browser's own Navigation Timing.

Decision

The third option. nginx adds Server-Timing: proxy;dur=…, app;dur=…, t;desc="…" to page responses: its own time on the request when the headers left, the part spent behind it (firewall and Next.js), and when. The home page reads the navigation entry after hydration and shows the visitor's request as a waterfall (DNS, connection and TLS, network and CDN, this server, download), the fields of the ledger record nginx wrote for it, and the commit the page was built from.

What it never shows: the client address in any form, the WAF anomaly score or matched rules, the remaining rate-limit allowance, upstream addresses or host names. Those would be a scoring oracle or abuse calibration, not a demonstration. The record's server-only values (t, rt, b) are shown as unknown rather than estimated.

A response that was stored and replayed (by a CDN or the browser cache) would carry another request's timing. The page compares the header's timestamp with the time the browser sent its request and, if the timing is older than two minutes, says the response was replayed instead of presenting it as the visitor's. Pages are served with cf-cache-status: DYNAMIC today, so this is a safeguard, not the normal path.

Consequences

  • No new data is collected, stored or transmitted beyond values the visitor's own response carries.
  • Origin processing time becomes visible to anyone, rounded to the millisecond. It is coarse, the same for every visitor, and the published reports already contain its distribution.
  • The component renders nothing until it has real values, and nothing after a client-side navigation, when the browser's entry describes a different document.
  • The user-agent classification shown is nginx's own list, copied into the page; a unit test reads nginx.conf and fails when they differ.

Source: docs/adr/0021-request-read-back.md

← All decisions