How to Use 103 Early Hints Behind a CDN

This walkthrough extends Resource Hints & Early Hints in Advanced Caching Strategies & CDN Architecture. Your origin takes 500ms to render a personalised page. During that half second the browser has an open connection and nothing to do. A 103 Early Hints response fills that window: the server sends Link headers immediately, the browser starts fetching the CSS and fonts, and by the time the HTML arrives those resources are already in flight or complete.

The technique is only worth deploying where that dead window actually exists, and it has one failure mode — hinting the previous deploy's filenames — that is worth designing against up front.

Rapid Diagnosis: Is There Dead Time to Fill?

  • Measure TTFB per route. Below roughly 200ms there is nothing to fill and the extra informational response can cost more than it saves.
  • Separate cache hits from misses. An edge-cached page has near-zero think time; the same route on a miss may have 600ms. Early Hints pays off on the miss.
  • Check what the browser does during the wait. In the Network panel, a long green "Waiting (TTFB)" bar with no other activity is exactly the window Early Hints targets.
  • Confirm end-to-end support. The client, the CDN and any intermediate proxy must all forward informational responses. One HTTP/1.1 hop in the chain can drop them silently.
Early Hints across browser, CDN and origin The browser requests a page, the CDN immediately returns a 103 with Link headers and forwards the request to the origin; the browser fetches the hinted CSS and font while the origin renders, then receives the 200 response. What happens during the 500ms wait Browser CDN edge Origin GET /account forwarded 103 Early Hints + Link fetching CSS + font still rendering 200 OK — CSS already parsed

Root Cause Analysis: Why Deployments Get This Wrong

1. Enabling it where TTFB is already fast. On an edge-cached page the HTML arrives in tens of milliseconds. The informational response adds protocol overhead with no window to exploit, and measurements show no improvement — which then gets misread as "Early Hints does not work".

2. Hinting stale filenames. Asset filenames are content-hashed and change on every build. Hints written by hand, or cached at the edge from a previous deploy, point at files that no longer exist — producing high-priority 404s during the most latency-sensitive moment of the page load.

3. Hinting too much. The temptation is to list every asset. Each hint competes with the HTML response that is about to arrive, and a dozen hinted files on a mobile connection will delay the document itself.

4. Assuming the chain preserves it. Informational responses require HTTP/2 or HTTP/3 support along the whole path, and some proxies drop them. Without an end-to-end check you may be sending hints nobody receives.

Step-by-Step Resolution

1. Generate the hint list from the build manifest. Never write asset paths by hand.

javascript
// build-hints.mjs — one Link header value per route, from the real manifest.
import { readFileSync, writeFileSync } from 'node:fs';
const manifest = JSON.parse(readFileSync('dist/manifest.json', 'utf8'));

const hints = Object.fromEntries(Object.entries(manifest.routes).map(([route, a]) => [
  route,
  [
    `<${a.css}>; rel=preload; as=style`,
    `<${a.font}>; rel=preload; as=font; type="font/woff2"; crossorigin`,
    '<https://img.example-cdn.com>; rel=preconnect'
  ].join(', ')
]));
writeFileSync('dist/early-hints.json', JSON.stringify(hints, null, 2));
// trade-off: this couples the edge config to the build output, so a deploy that
// updates one without the other breaks the hints. Ship them together as one
// artifact and version them with the same hash as the assets.

Expected outcome: hints can never point at a filename the current deploy does not have.

2. Emit the 103 at the edge, before the origin responds.

javascript
// Cloudflare Workers-style: hint immediately, then proxy the real request.
import hints from './early-hints.json' with { type: 'json' };

export default {
  async fetch(request, env, ctx) {
    const route = new URL(request.url).pathname;
    const link = hints[route] ?? hints['/'];
    // The platform emits 103 with these headers while the origin still works.
    const response = await fetch(request);
    const out = new Response(response.body, response);
    out.headers.set('Link', link);        // also honoured by clients ignoring 103
    return out;
  }
};
// trade-off: hints[route] is an exact match, so dynamic routes fall back to the
// home entry. Normalise the path to a route PATTERN before lookup, or the most
// personalised pages — the ones with the longest think time — get generic hints.

Expected outcome: hinted resources begin downloading during origin render, typically starting 300–500ms earlier on a slow route.

3. Keep the list to three or four entries. Critical CSS, one font, and one preconnect covers most pages. Anything else competes with the HTML.

4. Make the cache key correct. If your CDN caches the 103 separately from the 200, a deploy can leave them out of sync. Ensure both are keyed together, and verify after a deploy that changes the manifest:

bash
# Confirm the hinted asset paths match the ones the HTML actually references.
curl -sI --http2 https://example.com/account | grep -i '^link:'
curl -s https://example.com/account | grep -o '/assets/[a-z0-9.-]*\.\(css\|woff2\)' | sort -u
# trade-off: curl reports the Link header on the final response, not the 103.
# Use a browser trace or a protocol-level tool to confirm the informational
# response itself is being sent and forwarded.

Verification

Confirm three things in order. First, that the 103 is actually emitted and received — in Chrome DevTools the hinted requests start before the document response completes, which is visually unmistakable in the waterfall. Second, that the hinted resources are the right ones: every hinted URL should also appear in the HTML. Third, that TTFB-heavy routes improved and cached routes did not regress.

Track the win where it lands. Early Hints reduces the LCP resource-load-delay phase, not TTFB itself — TTFB is unchanged by design, because the origin still takes as long as it took. A team measuring only TTFB will conclude nothing happened.

json
{ "ci": { "assert": { "assertions": {
  "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
  "render-blocking-resources": ["error", { "maxNumericValue": 200 }],
  "server-response-time": ["warn", { "maxNumericValue": 600 }]
} } } }

Keep the server-response-time warning in place even after shipping Early Hints. Filling the think time is a mitigation; shortening it is the real fix, and it is easy to stop working on that once the symptom is masked.

FAQ

Does Early Hints replace preload in the HTML?

No — keep both. Clients that ignore the informational response still need the hints, and the Link headers on the final 200 (or the <link> tags in the head) cover them. The 103 is an accelerator for clients that support it, not a replacement for the hints themselves. Sending the same list twice costs nothing, because the browser deduplicates by URL.

Can I send Early Hints for a page served from cache?

You can, and it will rarely help. A cached page's HTML arrives so quickly that the browser discovers the same resources moments later on its own. The overhead is small, so it is not harmful, but the measured gain will be within noise — spend the configuration effort on the uncacheable, personalised routes where think time is real.

What happens if a hinted file no longer exists?

The browser issues a high-priority request that returns 404, wasting bandwidth and a connection slot at the worst possible moment, and the console logs an unused-preload warning as well. This is why the hint list must be generated from the same manifest that names the assets, and shipped in the same artifact — a manual list will eventually drift, and the failure is silent until someone reads the waterfall.

How much improvement should I expect?

Roughly the origin think time you manage to fill, bounded by how long the hinted resources take to fetch. On a route with a 600ms TTFB where the critical CSS takes 250ms to download, you recover close to that 250ms — the download finishes before the HTML even arrives. On a route with a 120ms TTFB there is nothing to recover. Measure TTFB per route first and enable the feature where the dead window is worth filling.

Can Early Hints hurt anything?

Two ways, both avoidable. Hinting too many resources means the informational response starts downloads that compete with the HTML itself, which arrives moments later. And hinting a stale filename produces a high-priority 404 during the most latency-sensitive part of the load. Keep the list to three or four entries generated from the build manifest, and neither failure mode can occur.

Do all clients and intermediaries support it?

Support is broad among current browsers and major CDNs, but the whole path must cooperate: an intermediate proxy that does not understand informational responses may drop them silently, and HTTP/1.1 hops cannot carry them at all. Because unsupported clients simply ignore the 103 and use the repeated Link headers on the final response, the failure mode is a lost optimisation rather than a broken page — which makes it safe to enable and worth verifying with a real trace rather than assuming.