ADR-0013 - Observed timing is authoritative; server timing is evidence
ADR-0013 - Observed timing is authoritative; server timing is evidence #
- Status: Accepted
- Date: 2026-05-07
- Deciders: Natan
Context #
SQA probes need timing for two different jobs:
- Operational truth. How long did the probe take from the place SQA
runs? This includes client scheduling, DNS, TCP/TLS, proxies, server work, response transfer, body parsing, and validation.
- Diagnosis. If an HTTP service reports its own timing in response
headers, how much time does the server claim it spent internally?
Those are related but not interchangeable. A server can report Server-Timing: app;dur=20 while the SQA probe observes 800 ms because the network path is slow, TLS negotiation is cold, the proxy is slow, or the response body is expensive to read. Conversely, a fast network can hide a slow internal phase unless the server exposes it.
The current runner already measures step duration with performance.now(). The Result envelope has a generic context field that can carry durations, but no standard field names. HTTP components currently ignore timing headers.
Decision #
SQA has two timing categories:
| Field | Source | Meaning |
|---|---|---|
observedStartedAt | SQA wall clock (Date) | ISO timestamp for correlation. |
observedCompletedAt | SQA wall clock (Date) | ISO timestamp for correlation. |
observedDurationMs | SQA monotonic clock (performance.now()) | Authoritative elapsed probe duration. |
serverTiming | HTTP Server-Timing header | Optional per-metric server-reported durations. |
serverReportedDurationMs | HTTP Server-Timing / known legacy headers | Optional summary of server-reported duration. |
observedDurationMs is the only authoritative duration for SQA thresholds, baselines, and regressions. It is measured around the full step call, including body read and validation.
observedStartedAt and observedCompletedAt are wall-clock timestamps for correlation with logs, deploys, and incidents. They are not used to compute duration because wall clocks can jump.
serverTiming and serverReportedDurationMs are optional HTTP evidence. They help explain where time went when the server exposes useful headers. They never replace observedDurationMs.
Implementation ownership:
src/lib/step.tsmeasures observed timing and attaches it to any
returned Result.
src/lib/result.tsowns the helper that merges timing into a Result
without losing existing context.
src/lib/http-timing.tsparses standardServer-Timingand a small
set of legacy response-time headers.
- HTTP components may include parsed server timing in their own
Result.context.
Consequences #
- Pro: Every check gets consistent observed timing without each
component implementing a stopwatch.
- Pro: Timing thresholds compare the experience SQA actually
observed, not a server's partial self-report.
- Pro: HTTP server timing remains available for diagnosis.
- Con: A component's own context can contain
serverReported*
fields while step() later adds observed* fields, so readers must understand the distinction.
- Con: Non-HTTP SDK probes only get observed timing unless the SDK
exposes its own server-side timing evidence later.
- Falsifiability: Revisit if a real threshold needs to alert on
server-side time independently of observed time. That should become a separate threshold over serverReportedDurationMs, not a replacement for observedDurationMs.
See also #
src/lib/step.ts↗ - observed timing owner.src/lib/result.ts↗ - Result envelope.src/lib/http-timing.ts↗ - HTTP timing
header parser.
- MDN:
performance.now()↗- monotonic high-resolution timing for durations.
- MDN:
Server-Timing↗- standard HTTP response header for server-reported timing.