HTTP/2 Server Push vs 103 Early Hints
If your build still generates HTTP/2 PUSH_PROMISE frames to accelerate first paint, you are shipping configuration that every current browser now ignores — and the job it was meant to do belongs to the 103 Early Hints interim response instead.
Root cause: push guessed for the client, Early Hints lets the client decide
HTTP/2 server push let the origin send a resource the client had not asked for: alongside the HTML response, the server opened extra streams carrying the stylesheet, the font, the script, each announced by a PUSH_PROMISE frame. On paper it removed a full round trip — the assets arrived before the browser’s parser had even discovered them. In practice it failed for one structural reason: the server pushed blind to the client’s cache. The browser is the only party that knows what is already in its HTTP cache, its memory cache, or its module map. A server pushing app.css to a returning visitor who already had app.css cached spent bandwidth and a stream on bytes the browser then discarded — and worse, the push often arrived ahead of the browser’s chance to send a RST_STREAM cancel, so the waste was already on the wire.
The problems compounded. Push interacted badly with stream prioritization: pushed streams competed with the HTML itself for connection bandwidth, so an over-eager push config could delay the very document it was trying to accelerate — a priority inversion the author never asked for. Push was also hard to reason about across a CDN, where the edge and origin disagreed about what had been pushed. By the time the ecosystem measured it at scale, push was as likely to hurt as help, and Chromium removed it in version 106.
103 Early Hints, defined in RFC 8297, keeps the one genuinely good idea — use the server’s think-time to start critical fetches early — and hands the decision back to the client. Instead of pushing bytes, the server flushes a header-only interim response carrying Link: rel=preload and rel=preconnect directives. The browser reads them, checks its own cache, and initiates the fetches it actually needs. The cache-blindness that doomed push simply cannot occur, because the party doing the cache lookup is the party that owns the cache.
The difference in one timeline
Minimal reproduction: the Early Hints replacement
Wherever your server configuration currently declares a push list, the equivalent is a 103 response carrying the same resources as Link headers, followed by the final 200 that mirrors them. Expressed as the raw response bytes on one HTTP/2 connection:
HTTP/2 103 Early Hints
link: </css/app.css>; rel=preload; as=style
link: </fonts/inter.woff2>; rel=preload; as=font; crossorigin
link: <https://img.cdn.example>; rel=preconnect
HTTP/2 200 OK
content-type: text/html; charset=utf-8
link: </css/app.css>; rel=preload; as=style
link: </fonts/inter.woff2>; rel=preload; as=font; crossorigin
<!doctype html>…
The Link headers are repeated on the 200 on purpose: a browser that ignored the interim response still acts on the final one, and edge platforms that learn hint sets read them from the final response. The browser deduplicates — a resource already fetched because of the 103 is not fetched again for the identical 200 hint. The crossorigin on the font preload matters for the same reason it always does: without it the interim-hinted fetch and the CSS-triggered fetch key to different cache entries and the font downloads twice.
Deterministic migration protocol
Move from a push configuration to Early Hints one step at a time, verifying at each stage that you have not regressed.
- [ ] 1. Inventory the current push list. Extract every resource your server or CDN currently pushes (often an
http2_pushdirective or aLink; rel=preload; nopush-adjacent config). This list is your candidate hint set. - [ ] 2. Prune to render-critical only. Push lists tend to accrete. Keep only resources needed for first paint — the render-blocking stylesheet, the LCP image, the critical font. Everything else belongs in markup or prefetch, not in the hint set.
- [ ] 3. Disable server push. Remove the push directives. Confirm in the Network panel that no request shows “Push / Other” as its initiator — current browsers ignore push, so this only removes dead configuration and its origin-side cost.
- [ ] 4. Emit the 103 with the pruned list. Configure the origin or edge to flush a
103carrying the kept resources asLink: rel=preload/rel=preconnect. See enabling Early Hints on CDN and origin for the server-specific steps. - [ ] 5. Mirror the hints on the 200. Add the identical
Linkheaders to the final response so cache-cold browsers and edge learners still benefit. - [ ] 6. Verify with
curl. Runcurl -v --http2 https://your-site/routeand confirm a103line appears before the200, with the expectedlink:headers on both. - [ ] 7. Confirm dedup and no unused preloads. Load the route in DevTools; each hinted resource should fetch once and be consumed. An unused-preload warning means an over-broad hint survived step 2.
Before/after metrics
Measured on a dynamic route with ~500 ms origin think-time serving a render-blocking stylesheet and a critical font, on a simulated 4G link. “Push” is the legacy config as seen by a current browser (push ignored); “Early Hints” applies the migration.
| Metric | Server push (current browser) | 103 Early Hints | Change |
|---|---|---|---|
| Critical CSS start time | after HTML parse (~560 ms) | during think-time (~60 ms) | −500 ms |
| Largest Contentful Paint | 3.1 s | 2.3 s | −0.8 s |
| Wasted pushed bytes (returning visitor) | up to full asset size | 0 | eliminated |
| Extra streams contending with HTML | 2 | 0 | −2 |
| First Contentful Paint | 1.9 s | 1.4 s | −0.5 s |
With push ignored by the browser, the legacy config delivered none of its intended head start — the stylesheet was still discovered only at parse time. Early Hints recovers the think-time overlap that push was originally chasing, without the cache-blind waste that got push removed in the first place.
FAQ
Is HTTP/2 server push still usable in any browser?
No. Chromium removed support for HTTP/2 server push in version 106, and other major browsers had already dropped or never shipped meaningful support. A PUSH_PROMISE frame sent to a current browser is ignored and the pushed stream is refused or cancelled. Because push required no client opt-in, servers can keep emitting it harmlessly, but no browser will act on it — so it delivers no benefit.
Does 103 Early Hints work over HTTP/3?
Yes. Early Hints is a status code, not a transport feature, so it rides on HTTP/2 and HTTP/3 the same way. Interim responses are part of the HTTP semantics both versions share. In practice it is used over HTTP/2 and HTTP/3 rather than HTTP/1.1, because interleaving an interim response with the eventual final response on one connection is cleanest under multiplexing.
Can Early Hints waste bytes the way push did?
Much less easily. Because the browser initiates the fetch, it first checks its own cache and skips anything already stored — precisely the check push could not perform. The residual waste case is hinting a resource the page does not actually use, which surfaces as an unused-preload console warning and is caught in review, not silently pushed onto every visitor.
Related
- 103 Early Hints Implementation — the interim-response semantics and hint-set design behind this migration
- Enabling 103 Early Hints on CDN & Origin — the server-by-server steps referenced in the protocol above