Speculation rules for evil

You can use speculation rules to make your site feel faster. You can also (but probably shouldn’t) use them to make other sites do work they didn’t ask for.

At scale, that means you can turn your traffic into a steady stream of requests against somebody else’s infrastructure, in a way that costs them time and money, and which degrades their UX.

The idea is simple enough. You give the browser a set of rules that describe which URLs a user might visit next, usually as patterns rather than hard-coded links, along with some hints about how eager it should be to fetch them. Under the right conditions, it goes off and requests those pages in advance. When the user clicks, the page is already there. It feels instant. Everyone wins.

Except the browser doesn’t care whose URLs match those patterns.

If your rules point to pages on another site, the browser will still request them. Those requests aren’t hypothetical, and they’re not some kind of lightweight preview. They’re real HTTP requests which hit the target server, run whatever code sits behind the page, query databases, and generate full responses. From the other site’s point of view, it looks a lot like a normal visit.

At small scale, that’s background noise. At larger scale, it starts to look like a pattern. A busy site, with a lot of users idling on pages, can quietly generate a steady stream of requests to arbitrary URLs. Not bots, not scripts, not obviously malicious. Just browsers following instructions that look entirely reasonable.

And crucially, doing it without slowing your site down, without degrading your user experience, and without any obvious signal that it’s happening.

Which is where this stops being a neat performance trick, and starts to look like a way of shifting work onto somebody else’s system.

How to turn a page view into background load

Once you stop thinking about speculation rules as a performance feature, and start thinking about them as a way of generating requests, the shape of this becomes clearer.

You’re not defining pages to preload. You’re defining patterns, and telling the browser how aggressively to pursue them.

That gives you a lot of room to manoeuvre.

You can match broad sets of URLs rather than individual pages. You can include query strings. You can describe whole classes of endpoints which are likely to be expensive to generate. And you can turn the eagerness up so that, given the slightest hint of user intent or idle time, the browser starts making requests.

Those requests don’t need to be likely next clicks. They just need to match the rules.

For example, you might match /search?q=* or /cart?add-to-cart=*, ensuring that each request bypasses cache and hits the application. Not because a user is about to visit those pages, but because they match a pattern you’ve defined.

Because you’re working with patterns, you can introduce variation. Change parameters, expand the surface area, reduce the chance of cache hits. The goal isn’t to warm anything up. It’s to ensure that each request is doing fresh work.

You don’t even need to declare all of this up front. Rules can be injected client-side, adjusted on the fly, and triggered opportunistically while the user is reading or watching something else. From their point of view, nothing is happening. From the target site’s point of view, it’s a steady stream of legitimate-looking requests.

Each request is small. Multiplied across real traffic, they add up to something more deliberate.

Why this is hard to see, and harder to stop

None of this looks like an attack.

The requests come from real users, on real devices, with normal browsers. They arrive from diverse IPs, across geographies, at a pace that looks like ordinary browsing behaviour. There’s no obvious spike, no single source to block, no bot signature to fingerprint.

From the outside, it just looks like traffic.

And unlike other ways of generating cross-site requests, this isn’t wrapped in anything suspicious. It’s not an iframe. It’s not a script hammering an endpoint. It’s a navigation-like request initiated by the browser itself, because it believes there’s a chance the user might want that page.

There are signals, if you know where to look. Headers, request context, subtle differences in how the browser initiates the fetch. And if somebody is being especially cynical, they can also suppress referrer data with a Referrer-Policy: no-referrer header, making attribution even harder. But these patterns are obscure, poorly understood, and rarely monitored.

So in practice, most sites treat this traffic exactly the same as any other request. They route it through the same application code, hit the same databases, and generate the same responses.

Which is the whole point.

This is trivial to implement

And this isn’t complicated. It’s a few lines of JSON.

Speculation rules can be declared in the page, or injected with a small piece of JavaScript. Define some patterns, set the eagerness, and the browser does the rest. Once it’s in place, every page view becomes an opportunity to generate requests in the background.

Which makes this less of a technical challenge, and more of a design decision.

You’re not building an attack system. You’re opting into a browser feature, and pointing it in a particular direction.

Who pays for it

Every request you generate has to be handled by the target site.

That means running application code, querying databases, assembling responses. On a static page behind a CDN, that might be cheap. On anything dynamic, it isn’t.

Ecommerce sites are an obvious example. Add-to-cart actions, product variations, search and filtering. These often bypass cache and hit the database directly. Multiply that by thousands of background requests and you’re not just increasing traffic, you’re increasing work.

That work translates into resource usage. More CPU, more memory, more concurrent processes. On modern hosting, that often means autoscaling. More instances, more cost. On smaller setups, it means slower responses, queueing, and eventually failure.

And none of this is driven by real demand.

These aren’t users trying to browse or buy. They’re browsers following speculative instructions from somewhere else. From the target site’s point of view, the distinction is invisible. The result is the same.

More load, more cost, less headroom.

Who can actually do this at scale

This only matters if you have traffic.

Speculation rules run in users’ browsers. The more users you have, and the longer they sit on your pages, the more opportunities you have to generate requests. Idle time becomes useful.

So scale isn’t about infrastructure. It’s about audience size.

A site with a few hundred visits a day can’t do much with this. A site with millions of sessions and long dwell times can generate a steady volume of background requests without trying very hard.

That creates a simple imbalance.

Large sites can generate load cheaply. Smaller sites have to handle it.

What you should do about it

Most systems will handle this traffic without question. They’ll treat it like any other request, run it through the same logic, and pay the cost.

That’s the first thing that needs to change.

Speculative requests don’t carry the same intent as real navigations. But unless you explicitly detect them, they look identical.

There are signals you can use. Headers, request context, and things like the Sec-Purpose header can help you identify requests triggered by speculation rather than user interaction.

Once you can see it, you have options.

At the extreme end, you might simply block it. Requests with signals like Sec-Purpose: prefetch, especially when combined with missing or suppressed referrer data, are unlikely to represent real user intent. Dropping or heavily limiting those requests is often reasonable.

Alternatively, you can deprioritise them, rate limit them, bypass expensive logic, or serve a cheaper response. The specifics will vary by stack, but the principle is simple.

Stop treating every request as equal.

An incomplete spec? 

Speculation rules are new-ish. They’ll mature, browsers will add guardrails, and the rough edges will get sanded down.

Right now, though, there’s a gap.

The spec already hints at this. There are plans for cross-site prefetching to require explicit opt-in via headers like Supports-Loading-Mode. But that safeguard doesn’t meaningfully exist today. In practice, browsers already perform cross-site prefetching in the wild, because features like Google preloading search results depend on it.

The browser is optimising for the user’s next click. It isn’t thinking about whether the target URL is expensive, whether it belongs to another site, or whether a million small acts of speculation might add up to something costly elsewhere.

That leaves site owners with an awkward job. They need to recognise that not all traffic carries the same intent, and that speculative requests may need different handling. Most aren’t doing that yet.

So for now, we have a performance feature which can quietly generate real work on other people’s servers, at scale, with almost no implementation cost, and very little scrutiny.

Useful.

And just a little bit dangerous.

0 Comments
Inline Feedbacks
View all comments