103 Early Hints Implementation

Every dynamically rendered page has a dead zone: the interval between the moment your server receives the request and the moment it flushes the first byte of HTML. During that think-time — database queries, template rendering, upstream API calls — the browser’s connection sits idle and its preload scanner has nothing to scan. On a route with 400 ms of backend latency, that is 400 ms in which the render-critical stylesheet, font, and LCP image could already be in flight but are not, because their URLs are trapped inside a document that does not exist yet. The 103 Early Hints interim response, defined in RFC 8297, closes this gap: the server (or CDN edge) flushes a header-only response containing Link preload and preconnect directives immediately, the browser starts fetching, and the final 200 arrives into a session where the critical subresources are already downloading.

This page covers the interim-response semantics that make 103 different from every other status code, which browsers act on it and under what transport constraints, the two CDN delivery models, what belongs in a hint set (and what actively hurts), a step-by-step origin and edge implementation, and the verification workflow that proves the hints are actually arriving and being used.


How an interim response works

HTTP allows a server to send any number of informational (1xx) responses before the final response on the same request. A 103 is exactly that: a status line and a header block, no body, followed later — on the same stream — by the real response. Three properties follow from the RFC 8297 semantics, and each one shapes implementation decisions:

It is not the final response. Headers on the 103 are purely advisory. The client must wait for the final response for everything authoritative — status, caching policy, content type, cookies. A 103 cannot set cookies, cannot redirect, and cannot be cached; HTTP caches never store interim responses.

It can be sent before routing finishes. Because the 103 commits the server to nothing, an origin or edge can emit it the instant the request line and Host header are parsed — before authentication, before the application framework even wakes up. This is what makes it powerful: the hints ride on knowledge of the route, not of the response.

The browser may act, or not. The specification says the client can speculatively process the headers as if they belonged to the final response. In practice, browsers act on exactly two Link relation types — preload and preconnect — and ignore everything else. Hinted preloads enter the same preload machinery as markup hints: same as-based priority mapping, same CORS mode rules, same typed-cache keying, same unused-preload warnings.

The sequence below shows where the win comes from — the hinted fetches overlap the origin’s think-time instead of queuing behind it.

103 Early Hints request sequence Three lifelines: browser, CDN edge, and origin. The browser sends a GET. The edge forwards it and immediately returns a 103 with Link preload and preconnect headers. While the origin spends 600 milliseconds rendering, the browser fetches the stylesheet and font from the edge cache in parallel. The final 200 with HTML arrives into a session where the critical assets are already local. Browser CDN edge Origin GET /product/42 (h2 stream 1) forward to origin server think-time ≈ 600 ms 103 Early Hints — headers only, ~200 bytes Link: preload style, font · preconnect img CDN GET /app.css (stream 3, u=0) 200 — served from edge cache GET /inter-var.woff2 (stream 5, u=2) 200 — font cached before HTML exists TLS to third-party image origin warmed via preconnect 200 OK + HTML body 200 OK — parser starts with CSS + font already local ≈ 600 ms of fetching moved off the critical path interim response hinted fetches origin think-time overlapped

Browser handling and engine differences

Support arrived engine by engine, and the constraints differ enough to matter for rollout decisions.

Transport: HTTP/2 and HTTP/3 only. No browser processes a 103 received over HTTP/1.1. The reason is ecosystem hygiene, not spec text: a generation of HTTP/1.1 intermediaries and client libraries mishandles unexpected 1xx responses — some treat the interim status line as the final one, leaving the real response stranded in the buffer. Over h2 and h3, interim responses are unambiguous HEADERS frames on the stream, so browsers act on them there and only there. Your origin-to-CDN leg can still run HTTP/1.1 if the CDN re-emits the hints on a multiplexed edge connection.

Navigation scope. Chromium processes Early Hints only for top-level navigations — not for iframes and not for subresource requests. It also acts on the first 103 per request and ignores subsequent ones, so consolidate all hints into a single interim response.

Relation types. All supporting engines act on rel=preload and rel=preconnect. Other relations — prefetch, modulepreload, dns-prefetch — are ignored in a 103 even where the same engine supports them in markup.

Behaviour Chromium (Blink) Safari (WebKit) Firefox (Gecko)
Acts on 103 Chrome 103+ Safari 16.4+ Preconnect from Firefox 116, preload from Firefox 126
Transport required h2 / h3 only h2 / h3 only h2 / h3 only
Scope Top-level navigation only Top-level navigation only Top-level navigation only
Multiple 103s First only First only First only
rel values honoured preload, preconnect preload, preconnect preload, preconnect
103 discarded after cross-origin redirect Yes Yes Yes
DevTools visibility “Early Hints Headers” section + early-hints initiator Limited (inspect via final response timing) Shown in Network header view

Spec and API reference

The Link header (RFC 8288 web linking syntax) carries one or more comma-separated links, each a URI reference in angle brackets followed by ;-separated parameters:

# One header, three links: two preloads and a preconnect. The browser maps
# each preload to the priority band its 'as' value dictates — identical to
# an equivalent <link> tag at the very top of <head>.
HTTP/2 103
link: </assets/app.css>; rel=preload; as=style,
      </fonts/inter-var.woff2>; rel=preload; as=font; crossorigin,
      <https://img.example-cdn.net>; rel=preconnect
Parameter Applies to Values Effect in a 103
rel required preload, preconnect honoured; others ignored Selects hint machinery
as preload style, script, font, image, fetch, document Sets typed cache slot and fetch priority band; required, or the preload double-fetches
crossorigin both flag or anonymous / use-credentials Must match the eventual consumer’s CORS mode; fonts always need it
fetchpriority preload high, low, auto Raises or lowers the priority within the as band, e.g. high for the LCP image
type preload MIME type Skips the fetch when unsupported (e.g. image/avif)
imagesrcset / imagesizes preload; as=image srcset/sizes syntax Responsive preload; the browser picks the candidate before layout

Browser support matrix

Feature Chrome Edge Firefox Safari
103 preload (navigation) 103 103 126 16.4
103 preconnect 103 103 116 16.4
fetchpriority param in hinted preload 103 103 132 17.2
imagesrcset in hinted preload 111 111 126 No
firstInterimResponseStart (Resource Timing) 115 115 No No

What belongs in a hint set — and what does not

A 103 fires before the server knows anything about the response beyond the route. That asymmetry defines the selection rules.

Hint these. The render-critical stylesheet; the one or two font files used above the fold (as=font; crossorigin); the LCP hero image with fetchpriority=high when its URL is stable per route; and preconnect for the two or three third-party origins that serve render-critical assets — the same origins you would target with a markup preconnect hint, but warmed hundreds of milliseconds earlier.

Do not hint these. Anything personalised or session-dependent — the 103 is emitted before auth runs, so a hint that varies by user either leaks information or fetches the wrong variant. Speculative next-page assets: rel=prefetch is ignored in a 103, and stuffing low-value preloads in instead burns the early bandwidth window on bytes that do not unblock render. Large below-the-fold images. And long lists generally: hinted preloads compete inside the same multiplexed connection as the HTML that will soon stream back, so a 12-asset hint set can starve the document itself — the same stream prioritization pressure that over-preloading causes in markup, amplified because the fetches start earlier. Cap the set at roughly five links.

The payoff scales with think-time. If the route is edge-cached and the final response follows the 103 by ten milliseconds, the hints buy ten milliseconds. Spend the implementation effort on slow dynamic routes — checkout, search, personalised landing pages — where the gap is 300–1000 ms.


Implementation steps

Step 1 — Quantify the think-time budget

In Chrome DevTools → Network, select the document request and read the Waiting for server response segment in the Timing tab. Sample p50 and p90 for each candidate route from RUM or CDN logs. Routes under ~100 ms of origin wait go to the bottom of the list.

Step 2 — Freeze the hint set per route

From the same waterfall, take the render-blocking CSS, above-fold fonts, and LCP image, and record their URLs alongside the route. Fingerprinted asset URLs (app.3f8a1c.css) change on deploy, so wire the hint set into your build output rather than hand-maintaining it.

Step 3 — Emit the 103 at the origin

Node.js has first-class support since version 18.11 via writeEarlyHints:

// server.js — flush hints before any async work begins. The whole point
// is that these headers leave the socket while the database query and
// template render are still pending, so call this first, synchronously.
import { createServer } from 'node:http';

const HINTS_BY_ROUTE = new Map([
  ['/product', [
    '</assets/app.css>; rel=preload; as=style',
    '</fonts/inter-var.woff2>; rel=preload; as=font; crossorigin',
    '<https://img.example-cdn.net>; rel=preconnect'
  ]]
]);

createServer(async (req, res) => {
  const hints = HINTS_BY_ROUTE.get(req.url.split('?')[0]);
  if (hints) {
    // Interim response: status line + Link headers, no body, stream stays open.
    res.writeEarlyHints({ link: hints });
  }

  const html = await renderPage(req); // the think-time the hints overlap

  // Mirror the hints on the final response: browsers that ignored the 103
  // still preload from here, and learning CDNs read the hint set from it.
  res.writeHead(200, {
    'content-type': 'text/html; charset=utf-8',
    link: hints ? hints.join(', ') : ''
  });
  res.end(html);
}).listen(8080);

Other origin stacks: Apache httpd emits 103 from Link headers when H2EarlyHints on is set (2.4.30+); h2o generates them natively from configured Link response headers; HAProxy has an early-hint action; nginx gained the ability to forward upstream 103s with the early_hints directive in recent releases but cannot originate hints itself from plain config. The per-server details and failure modes are covered in Enabling 103 Early Hints on CDN & Origin.

Step 4 — Enable the CDN layer

CDNs deliver Early Hints under one of two models, and you must know which yours uses because the operational behaviour differs:

  • Edge-generated (learned or configured). The edge observes Link headers on your final responses (or takes a static per-route config) and emits the 103 itself on subsequent requests, before contacting the origin at all. This gives the fastest possible hint delivery — one edge RTT — and works even when the origin cannot speak 103. The cost: the hint set lags one response behind reality, which matters on deploys.
  • Origin passthrough. The edge forwards the origin’s real 103 to the browser. Hints are always current, but they arrive no earlier than one full origin RTT, and every hop between edge and origin must forward interim responses.

Config-level enablement is typically a per-zone or per-route toggle plus, for the passthrough model, confirmation that the origin protocol and any intermediate load balancers preserve 1xx responses.

Step 5 — Tune priorities inside the hint set

Hinted preloads land in the normal priority queue, so shape them: fetchpriority=high on the LCP image preload, nothing elevated on secondary assets. If the document response later competes with a hinted download for bandwidth, prefer letting the HTML win — it unlocks everything else.


Verification workflow

curl. Interim responses print inline over HTTP/2:

# --http2 is load-bearing: over --http1.1 most edges suppress the 103
# entirely, because no browser would act on it there anyway.
curl -sv --http2 https://www.example.com/product/42 -o /dev/null 2>&1 | grep -A4 '< HTTP/2 103'

You should see < HTTP/2 103 followed by one or more < link: lines, then later < HTTP/2 200.

Chrome DevTools. Open Network, click the document request → Headers. When a 103 was received, an Early Hints Headers section appears above the response headers. Then find the hinted assets in the waterfall: their Initiator column reads early-hints, and their request start precedes the document’s response start — that inversion is the entire feature, visible in one glance.

Resource Timing. In Chromium, performance.getEntriesByType('navigation')[0].firstInterimResponseStart returns a non-zero timestamp when a 103 arrived; finalResponseHeadersStart marks the 200. The delta is your measured think-time overlap, and it belongs in your RUM dashboard next to LCP.

WebPageTest. Run the route and read the waterfall: hinted requests appear as rows starting during the document request’s “waiting” band rather than after its content download begins. Compare median LCP across a test batch with hints on and off; the improvement should approximate the think-time overlap for cold-cache visitors.


Edge cases and gotchas

Caches never store the 103. Only final responses are cacheable. If you rely on edge-generated hints, understand the regeneration source — usually the last-seen final response’s Link header — and its staleness window. After a deploy that renames fingerprinted assets, a learning edge serves one round of hints pointing at dead URLs: the browser preloads 404s, wastes bandwidth, and the page loads at un-hinted speed. Purge or re-prime after deploys.

Mismatched hints double-fetch. The 103 preload obeys the same matching rules as markup preloads: wrong as, missing crossorigin on a font, or a URL that differs from the document’s eventual reference by so much as a query string produces a preloaded resource the parser cannot reuse — one wasted fetch plus the real one. Because the hint lives in server config far from the markup, this drift is easier to introduce and harder to spot than with inline <link> tags. The unused-preload console warning is your tripwire.

Redirects void the hints. If the final response turns out to be a redirect, the hinted work is at best wasted and — for cross-origin redirects — actively discarded by the browser for security. Never emit a 103 on routes that habitually redirect (auth walls, geo-splitting, trailing-slash canonicalisation); hint the redirect target instead.

Server asymmetry. Origin stacks differ sharply: h2o and Apache generate 103s from config, Node and most modern app frameworks expose an API, while nginx (older than the early_hints directive) and many managed load balancers silently drop interim responses coming from upstream. A passthrough chain is only as good as its weakest hop — one buffering proxy and the browser never sees the 103, with no error logged anywhere.

Connection-level interactions. Hints arrive on the same h2/h3 connection as everything else; on lossy networks the extra early streams slightly raise head-of-line pressure on HTTP/2 (not HTTP/3, where streams recover independently). If you operate at the edge, review CDN edge tuning for QUIC and HTTP/3 so the transport settings and the hint strategy pull in the same direction.


FAQ

Is 103 Early Hints worth it when TTFB is already fast?

Rarely. The benefit is strictly bounded by server think-time: hints that precede the final headers by 30 ms buy at most 30 ms of parallel fetching. Deploy Early Hints on dynamic, uncached routes where the origin needs hundreds of milliseconds, and skip edge-cached routes where the 200 follows almost immediately.

Why does my 103 never appear in the browser?

Check, in order: the navigation used HTTP/1.1 (browsers only act on 103 over h2/h3 — verify the Protocol column in DevTools); an intermediary between origin and browser dropped the interim response; the CDN’s Early Hints feature is off; or the request was an iframe or subresource, which Early Hints does not cover. Testing the origin directly with curl separates origin emission problems from delivery problems.

Can Early Hints preload resources that end up unused?

Yes, and it is the most common production regression. Hints fire before the HTML exists, so a hint set that drifts from the deployed markup — a renamed CSS fingerprint, a swapped hero image — fetches bytes the document never consumes. The browser logs the standard unused-preload warning. Regenerate hint sets from build output on every deploy.

Does a 103 response get cached?

No. HTTP caches store only final responses; interim responses are transient by definition. CDNs that serve hints on cache hits reconstruct the 103 from configuration or from Link headers seen on earlier final responses.

Yes. Browsers that ignored the interim response still act on the final response’s Link headers, and edge platforms that learn hint sets read them from there. Duplication is safe — a resource already fetched via the 103 is deduplicated against the 200’s identical hint.