npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sdxc/server-timing

v2026.10.13

Published

Server-Timing measurements collected per request and written to a response header

Readme

@sdxc/server-timing

Server-Timing measurements collected per request and written to a response header.

The Server-Timing header shows which part of a response was slow, right in the browser's network panel. A TimingCollector lives for one request, measure wraps each operation worth timing, and toHeaders writes the header.

Installation

npm add @sdxc/server-timing

Usage

Measure and report

import { TimingCollector } from "@sdxc/server-timing";

let timing = new TimingCollector();

let user = await timing.measure("db", "findUserById", () => findUserById(userId));

return Response.json(user, { headers: timing.toHeaders() });
// Server-Timing: db;desc="findUserById";dur=12.34

Time several operations

Each measure call adds one entry, in the order the calls finish. Keep the name short and stable (db, cache, auth) and put the varying part in the description. Both reach every client that reads the response.

let timing = new TimingCollector();

await timing.measure("auth", "authorize", () => authorize(request));
await timing.measure("cache", "lookup", () => cache.get(key));
await timing.measure("db", "listPosts", () => listPosts());
await timing.measure("render", "", () => renderPage());

timing.toString();
// auth;desc="authorize";dur=4.10, cache;desc="lookup";dur=1.02, db;desc="listPosts";dur=8.77, render;dur=3.20

An empty description leaves desc out of the entry.

Annotate an existing response

toHeaders writes into the Headers it receives and returns them:

let response = new Response(html, { headers: { "Content-Type": "text/html" } });

timing.toHeaders(response.headers);

return response;

API

new TimingCollector()

An empty set of measurements for one request. Create one per request so each response reports only its own timings.

collector.measure<T>(name: string, description: string, fn: () => Promise<T>): Promise<T>

Times fn and returns what it resolves to. The entry is recorded whether fn resolves or rejects, and a rejection is re-thrown unchanged.

collector.toString(): string

The header value: every entry comma-separated, or an empty string when nothing was measured. An entry is name;desc="description";dur=ms, with the description quoted and escaped, the duration rounded to two decimals, and dur present once the duration is above zero. Some server runtimes advance the clock only across I/O, so CPU-only work can read as zero there.

collector.toHeaders(headers?: Headers): Headers

Sets Server-Timing on headers (a new Headers when omitted) and returns the same object. It replaces any Server-Timing already present, so calling it twice leaves one copy.

Pattern: Timing every request

A wrapper gives each request a collector and writes the header after the handler answers. Copying into a new Response also covers responses with immutable headers, such as one returned by fetch or Response.redirect.

import { TimingCollector } from "@sdxc/server-timing";

export function withTiming(
	handler: (request: Request, timing: TimingCollector) => Promise<Response>,
) {
	return async (request: Request) => {
		let timing = new TimingCollector();
		let response = await handler(request, timing);

		return new Response(response.body, {
			status: response.status,
			statusText: response.statusText,
			headers: timing.toHeaders(new Headers(response.headers)),
		});
	};
}

export default {
	fetch: withTiming(async (request, timing) => {
		let posts = await timing.measure("db", "listPosts", () => listPosts());
		return Response.json(posts);
	}),
};

Pattern: Measuring across layers

Pass the collector down so helpers add their own entries to the same header. A measure around a helper adds an entry covering everything inside it, recorded after the inner ones.

import { TimingCollector } from "@sdxc/server-timing";

async function loadDashboard(userId: string, timing: TimingCollector) {
	let user = await timing.measure("db", "findUserById", () => findUserById(userId));
	let feed = await timing.measure("cache", "feed", () => cache.get(`feed:${userId}`));
	return { user, feed };
}

let timing = new TimingCollector();
let data = await timing.measure("load", "dashboard", () => loadDashboard(userId, timing));

return Response.json(data, { headers: timing.toHeaders() });
// Server-Timing: db;desc="findUserById";dur=12.34, cache;desc="feed";dur=1.02, load;desc="dashboard";dur=13.51

Pattern: Reporting a failed call

A rejected operation still produces its entry, so an error response can show how long the failing call took before it gave up.

import { TimingCollector } from "@sdxc/server-timing";

let timing = new TimingCollector();

try {
	let rates = await timing.measure("upstream", "rates", () =>
		fetch("https://api.example.com/rates").then((response) => response.json()),
	);
	return Response.json(rates, { headers: timing.toHeaders() });
} catch {
	return new Response("Upstream unavailable", {
		status: 502,
		headers: timing.toHeaders(),
	});
	// Server-Timing: upstream;desc="rates";dur=5000.12
}

Versioning

Releases are dated rather than semantic. A version is the UTC date it was published, written YYYY.M.D, so 2026.9.4 is the release from 4 September 2026. At most one release goes out per day.

Those numbers say when, not what: a later date means a later release and carries no compatibility promise. Any release may change or remove an export.

Depend on one exact date, and move it when you are ready to take the change:

{
	"dependencies": {
		"@sdxc/server-timing": "2026.9.4"
	}
}

A caret or tilde range reads the date as major, minor and patch, so it accepts every later release in the same year. An exact version keeps the upgrade yours to schedule.

License

MIT

Author

Sergio Xalambrí