
An Unobtrusive Signal from the Preload Scanner
Stop letting speculative browser requests pollute your analytics and waste server resources by leveraging the Sec-Purpose header.
Imagine you’re tailing your production logs and you notice a spike in traffic to an expensive /api/dashboard-summary endpoint. You check your analytics dashboard, but the active user count is flat. You aren’t being DDoSed, and your users haven’t suddenly become hyperactive. Instead, you’re likely seeing the "psychic intern" of the web—the browser’s preload scanner—doing its job a little too well.
Modern browsers are incredibly impatient. They don't wait for the HTML parser to finish its work before they start hunting for resources. They look ahead, find link tags, and fire off requests for assets they *think* the user might need next. While this makes the web feel fast, it also means your server is often doing heavy lifting for pages that never actually get rendered.
The Secret Handshake: Sec-Purpose
Chromium-based browsers have started being much more polite about these speculative requests. They now include a specific HTTP header that tells your server exactly why a request is happening: Sec-Purpose: prefetch.
Sometimes you'll see variations like Sec-Purpose: prefetch;prerender. This is the browser saying, "Hey, I'm just grabbing this in case the user clicks it. Don't go crazy."
If you aren't looking for this header, you’re treating a "maybe" request with the same urgency as a "definitely" request. That's a waste of your CPU cycles and, more importantly, it ruins the integrity of your server-side analytics.
Detecting Speculative Requests in Node.js
If you're running an Express or Koa server, catching these requests is straightforward. You can write a tiny piece of middleware to identify these "ghost" visitors and handle them differently.
const express = require('express');
const app = express();
const detectPrefetch = (req, res, next) => {
// Check for the Sec-Purpose header
const purpose = req.get('Sec-Purpose') || req.get('X-Purpose') || req.get('Purpose');
if (purpose && purpose.includes('prefetch')) {
req.isSpeculative = true;
// Log it, but maybe don't trigger that expensive DB query
console.log(`[Speculative] Prefetch detected for: ${req.url}`);
}
next();
};
app.use(detectPrefetch);
app.get('/api/data', (req, res) => {
if (req.isSpeculative) {
// Return a lightweight version or just a 204 No Content
// if the browser supports it, or serve from cache only.
return res.status(200).set('Cache-Control', 'max-age=600').json({ data: 'cached-lite' });
}
// Normal expensive processing here
res.json({ data: 'full-heavy-payload' });
});I've included X-Purpose and Purpose in that check because, while Sec-Purpose is the current standard, some older implementations and proxies still use the older variants.
Protecting Your Analytics
One of the biggest headaches with prefetching is "ghost conversions." If your server-side code triggers a "Page Viewed" event in your CRM or database the moment a route is hit, your conversion rates are going to look like garbage. You’ll see thousands of "views" with zero seconds of dwell time.
Here is how I usually handle this in a typical controller:
async function handleProfilePage(req, res) {
const user = await db.users.find(req.params.id);
// Stop the "ghost" analytics!
if (req.header('Sec-Purpose') !== 'prefetch') {
await analytics.trackView('Profile Page', { userId: user.id });
}
res.render('profile', { user });
}By adding that simple if statement, you ensure that your business metrics reflect actual human eyes on the screen, not just a Chromium background process trying to be helpful.
The Nginx Layer
If you want to be even more aggressive, you can handle this at the load balancer level. If your server is struggling under the weight of these speculative requests, you can rate-limit them or even block them for specific resource-intensive paths.
server {
location /heavy-report {
# Check the Sec-Purpose header
if ($http_sec_purpose ~* "prefetch") {
# Return a 'Service Unavailable' or 'No Content'
# to prevent the backend from even seeing the request.
return 204;
}
proxy_pass http://backend_servers;
}
}Is returning a 204 mean? Maybe. But if the /heavy-report takes 5 seconds to generate and costs $0.50 in compute time, you probably don't want to run it just because a user hovered over a link for 200ms.
When to Listen (and When to Ignore)
Don't go out and block every prefetch request. The preload scanner is a net positive for the web. It hides latency and makes transitions feel instant. The goal isn't to break the feature, but to be selective.
I generally follow these rules:
1. Static Assets: Let the prefetcher do whatever it wants. Images, CSS, and JS are usually cached and cheap to serve.
2. GET Requests with Side Effects: First of all, don't do this. But if you have them, absolutely block prefetching.
3. Expensive Data Aggregation: Use the header to serve a "lite" version of the data or a cached snapshot.
4. Analytics: Always gate your tracking pixels and logging behind a check for Sec-Purpose.
The browser is trying to help us out by being proactive. By acknowledging the Sec-Purpose signal, we can stop the wasted effort and keep our server logs—and our sanity—a whole lot cleaner.

