demo · v138

Server-side tuning by Sec-Purpose

The point of the header is so the server can do different things on a speculative request. Watch a simulated origin route each request: skip A/B test bucketing, skip analytics, return a slimmer body for speculative GETs — and run the full path on the eventual real navigation.

request as the server sees it

GET /article/138-features HTTP/2
Host: example.com
User-Agent: Chrome/138
Accept: text/html
Sec-Fetch-Dest: document

server’s decision tree

awaiting a request…
timerequestSec-Purposeserver behaviour

the routing logic

app.get("/article/:slug", (req, res) => {
  const sp = req.headers["sec-purpose"];
  const isSpec = sp?.includes("prefetch") || sp?.includes("prerender");

  // A/B test bucket assignment: only for real navigations
  if (!isSpec) assignBucket(req);

  // Analytics: don't fire on speculative
  if (!isSpec) trackPageview(req);

  // Cache-Control: speculative responses should be revalidated cheap
  res.set("Cache-Control", isSpec ? "private, max-age=30" : "private, max-age=300");

  // Body: speculative may return a "warming" payload that excludes
  // personalised recommendations; promote runs the full path.
  res.send(renderPage(req, { skipPersonalised: isSpec }));
});

Before 138, <link rel=prefetch> arrived as a normal GET. Servers had to either treat every page-fetch as speculative (costly) or run all the side effects on every prefetch (corrupting A/B tests and analytics). With Sec-Purpose, the server can branch precisely.

see also