HTTP/2 Server Push Is Dead. Early Hints Won by Doing Less

Server Push was HTTP/2's marquee feature: send files before the browser asks. Chrome ripped it out in 2022 after finding it used on 0.7% of sites and often making things slower. What replaced it did the opposite thing, and that's why it worked.

When HTTP/2 shipped in 2015, Server Push was the feature everyone put on the slides. The pitch was irresistible: instead of sending a browser an HTML page and then waiting for it to parse the page, discover it needs style.css, and ask for it in a second round trip, the server could just push style.css down the pipe alongside the HTML. You already know the browser is going to want it. Why make it ask? Skip the round trip. Free speed.

Seven years later, in Chrome 106 — beta on September 1, 2022 — Google took Server Push out and threw it away. Not deprecated-but-supported. Removed. Chrome stopped accepting pushed streams at all. Edge followed in the same release; Firefox pulled it in Firefox 132 in October 2024. The feature that was supposed to be HTTP/2’s headline is now something no major browser will accept.

What happened is one of the better lessons in protocol design, and it isn’t “Server Push was a dumb idea.” It wasn’t. It’s that the thing that replaced it wins by doing less, and understanding why is worth more than any performance checklist.

The number that killed it

Before the reasons, the number. When Chrome’s team went looking at how much Server Push was actually used, they found it on 1.25% of HTTP/2 sites — and when they reran the analysis later, that had dropped to 0.7%. Seven years after shipping the marquee feature, more than 99% of sites had declined to use it, and the tiny fraction that did were drifting away.

That alone would be a reason to cut maintenance. But the part that made removal a win rather than a shrug was the performance data. Chrome’s own analysis of Push showed, in their words, mixed results — no clear net performance gain, and in many cases performance regressions. The feature designed to make pages faster was, in the field, frequently making them slower. When your speed feature isn’t reliably fast and almost nobody uses it, keeping it is all cost.

So why did the obvious optimization — send the file before it’s asked for — go wrong in practice?

The cache the server couldn’t see

Here’s the flaw, and it’s structural, not a bug anyone could patch.

The whole premise of Push is that the server decides what the browser needs and sends it unprompted. But the server is missing the one piece of information that makes that decision correctly: what’s already in the browser’s cache. On a repeat visit, the browser already has style.css. It doesn’t need it. But the server, pushing blind, has no idea — so it pushes the file anyway, shoving bytes down the connection that the browser will take one look at and discard. The optimization meant to save bandwidth spends it, on exactly the returning visitors you most wanted to be fast for.

It gets worse, because there was no clean way to say “stop.” By the time the browser realizes it’s being pushed something it already has, the server has often already started sending the bytes. HTTP/2 lets you cancel a stream with RST_STREAM, but that’s a cancel after the send is underway — the bytes are already on the wire, the bandwidth already spent. You could reset the push, but you couldn’t un-send it. Push committed the server to a guess and gave nobody a good way to take the guess back.

And the guess was hard to time correctly, too. To actually save the round trip, you had to push during server think-time — the window while your backend is generating the HTML, before the response goes out. Push too late and the browser has already asked for the file itself; push the wrong things and you’ve delayed the HTML behind assets it’s fighting for bandwidth with. Implementations across nginx, Apache, and the CDNs all had their own sharp edges here. Server Push turned out to be a feature that was easy to enable and genuinely hard to make faster than doing nothing.

Early Hints inverts the whole thing

The replacement Chrome pointed everyone to is 103 Early Hints, standardized in RFC 8297 back in 2017 and shipped in Chrome 103 in June 2022 — a few versions, as it happens, before the one that removed Push. And the elegant part is that it uses the exact same opportunity Push was chasing — server think-time — to do almost the opposite thing.

While your backend is still working on the real response, the server sends an interim 103 Early Hints status with a few Link headers:

103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </app.js>; rel=preload; as=script

Then, later, the real 200 with the HTML follows. That’s it. The 103 isn’t the response — it’s a heads-up. It doesn’t send style.css. It sends the name of style.css and a suggestion: “you’ll probably want this; start fetching it if you like.”

Look at what moved. Push had the server decide and send. Early Hints has the server suggest, and lets the browser decide and fetch. And the browser is the one party that actually knows what’s in its own cache. If it already has style.css, it ignores the hint — no wasted bytes. If it doesn’t, it starts the request during the same think-time window Push was exploiting, so you still skip the round trip. You get Push’s upside — using idle server time to warm up the assets — without its fatal flaw, because the decision now lives with the party that has the information to make it.

That’s the whole story in one line: Push guessed on behalf of a cache it couldn’t see. Early Hints hands the guess to the one who can. The optimization that fights the browser’s cache loses to the browser’s cache. The optimization that cooperates with it wins.

The lesson that outlives the feature

It would be easy to file this under “another web feature that didn’t pan out” and move on. But there’s a design principle here that keeps coming up, and it’s worth naming: put the decision where the information is.

Server Push failed because it moved a decision — do you need this resource? — to the one place structurally guaranteed not to know the answer. The server can see the request; it cannot see the cache. No amount of tuning fixes a decision made in the wrong location. Early Hints didn’t succeed by being cleverer or faster; it succeeded by moving that same decision one hop, to the browser, where the answer actually lives, and then getting out of the way.

You see the same shape all over infrastructure once you look for it. Caching layers that guess at invalidation instead of being told. Load balancers routing on stale health data. Retry logic that decides to retry without knowing whether the first attempt actually landed. Every one of them is a decision made by a component that can’t see what it needs to see, and every one of them fails in the same quietly expensive way Push did — not with an error, but with waste that looks like it’s working.

Server Push isn’t coming back; there’s nothing to bring back to. But the reason it’s gone is the useful part. When you’re about to have one component decide something on another’s behalf, ask the boring question first: does the deciding component actually have the information to decide well? If it doesn’t, you haven’t built an optimization. You’ve built a guess, and you’ll pay for it on exactly the traffic you were trying to speed up.

Continue the conversation

← Back to Blog