Server-Timing
Server-Timing is a W3C header
browsers render in their Network panel (DevTools → Network → Timing
tab) — a per-request breakdown of where the time went. It rides on the
HTTP transport as an interceptor: importing
ServerTimingModule attaches one that always appends a final
total;dur=<ms>; handlers add their own sub-steps by recording into a
per-request Timings accumulator pulled from the request extensions.
This is observability for the frontend developer’s browser, not a replacement for proper tracing — for cross-service spans and aggregations, reach for OpenTelemetry.
Install
Section titled “Install”cargo add nest-rs-server-timingMount it
Section titled “Mount it”use nest_rs_core::module;use nest_rs_server_timing::ServerTimingModule;
#[module(imports = [ServerTimingModule])]pub struct ApiModule;Every response now carries a Server-Timing header. The minimum,
unstructured form is one entry:
$ curl -i http://localhost:3002/posts -H "Authorization: Bearer $TOKEN" | grep -i server-timingserver-timing: total;dur=12.34That alone shows up in DevTools next to the request — round-trip vs. server time becomes a glance.
Record sub-steps
Section titled “Record sub-steps”Pull Timings out of the request extensions and call record (or
record_with_desc) with a duration. The interceptor concatenates every
recorded entry into the final header, with total appended on the way
out:
use std::time::Instant;
use nest_rs_http::{controller, routes};use nest_rs_server_timing::Timings;use poem::Request;
#[controller(path = "/posts")]pub struct PostsController { #[inject] posts_svc: Arc<PostsService>, #[inject] media_svc: Arc<MediaService>,}
#[routes]impl PostsController { #[get("/")] async fn list(&self, req: &Request) -> Result<Json<Vec<Post>>> { let timings = req.extensions().get::<Timings>().cloned().unwrap_or_default();
let started = Instant::now(); let posts = self.posts_svc.feed().await?; timings.record("db", started.elapsed());
let started = Instant::now(); let posts = self.media_svc.with_cover_urls(posts).await?; timings.record_with_desc("covers", "object-store lookup", started.elapsed());
Ok(Json(posts)) }}Each timed span wraps a service call — the handler measures, it never computes. Attributing a slow request across two backends is the case this header exists for; a controller that also transformed rows would be timing its own layering mistake.
$ curl -i http://localhost:3002/posts -H "Authorization: Bearer $TOKEN" | grep -i server-timingserver-timing: db;dur=7.82, covers;dur=0.41;desc="object-store lookup", total;dur=8.96record(name, dur)— short entry;namemust be a valid header token (no spaces, no commas).record_with_desc(name, desc, dur)—descshows as a tooltip in DevTools; any string is allowed, the formatter quotes/escapes for you.
Going further
Section titled “Going further”- OpenTelemetry — for spans the frontend cannot see (cross-service traces, async work after the response, queue jobs).
- Interceptors — the seam
ServerTimingModuleplugs into; the same shape any other cross-cutting concern uses.
Reference
Section titled “Reference”crates/nest-rs-server-timing/—ServerTimingModule,Timings,Entry.
Built by YV17labs