103 Early Hints (tested)
103 Early Hints is an informational response a server sends before the real one, usually carrying Link: rel=preload or rel=preconnect headers so the browser can start fetching CSS, fonts or scripts while the server is still building the page. We sent real 103s from Node.js 24 over HTTP/1.1 and HTTP/2, delayed the 200 by one second, and logged exactly when each client asked for the preloaded file.
1. Sending a 103 from Node.js
Node.js 24.21.0 has response.writeEarlyHints() on both http and http2 servers. Our test handler sent the hint, waited 1,000 ms (standing in for a slow database query), then sent the page:
function handler(req, res) {
if (req.url === '/') {
res.writeEarlyHints({ link: '</style.css>; rel=preload; as=style' });
setTimeout(() => {
res.writeHead(200, { 'content-type': 'text/html' });
res.end('<!doctype html><link rel=stylesheet href=/style.css><p>hi');
}, 1000);
}
}
The same handler worked unchanged with http.createServer and http2.createSecureServer.
2. What curl shows
curl -v http://127.0.0.1:8983/ printed two complete header blocks on one request:
< HTTP/1.1 103 Early Hints
< Link: </style.css>; rel=preload; as=style
<
< HTTP/1.1 200 OK
< content-type: text/html
< Connection: keep-alive
< Transfer-Encoding: chunked
Over HTTP/2 (curl -kv --http2 https://127.0.0.1:8984/) it was HTTP/2 103 with a lowercase link: header, then HTTP/2 200. Note the 103 carries only the headers you pass to writeEarlyHints; it has no body and does not replace the final response. If you only look at the last header block (for example with curl -I output piped to grep), you will not notice the 103 at all.
3. What Node.js clients expose
| Client | Sees the 103? | Measured |
|---|---|---|
http.get | Yes, as an 'information' event | information 103 { link: '</style.css>; rel=preload; as=style' } at 16 ms, response 200 at 1,013 ms |
Built-in fetch() | No | Resolved once with status 200 at 1,016 ms; no link header in the result |
http.get(url, res => { /* the 200 */ })
.on('information', info => console.log(info.statusCode, info.headers));
If you need to test that your server really sends Early Hints, use curl -v or http.get with the 'information' listener. fetch() silently skips 1xx responses.
4. Does Chromium act on it? HTTP/1.1 vs HTTP/2
We loaded the page in headless Chromium 153 and logged, on the server, when /style.css was requested relative to the page request. The 200 was held back for 1,000 ms, so a CSS request well under 1,000 ms can only have come from the 103.
| Protocol | Server saw /style.css at | Acted on the 103? |
|---|---|---|
HTTP/1.1, http://127.0.0.1:8983 | 1,018 ms (once, after the HTML arrived) | No |
HTTP/2, https://localhost:8984 (self-signed, ignoreHTTPSErrors) | 14 ms, then again at 1,018 ms | Yes, preload started ~1 s before the HTML |
This matches Chromium's documentation (from the docs, not measured beyond this test): Chrome only processes Early Hints on HTTP/2 or HTTP/3 navigations, and ignores them on HTTP/1.1. A plain HTTP/1.1 origin gets no benefit, so measure through the protocol your users actually get (often HTTP/2 or HTTP/3 at a CDN edge).
Two things we saw but did not dig into:
- Over HTTP/2 the stylesheet was requested twice: the early preload at 14 ms and a normal request at 1,018 ms. The likely reason is that our self-signed certificate (accepted via
ignoreHTTPSErrors) stops Chromium from reusing the preloaded response; we did not verify this. On a real site, check the Network panel or your access log for a duplicate request before declaring a win. - Playwright's
page.on('request')did not report the early preload at all, and the page'sperformanceresource timing listed the CSS starting at 1,044 ms. Server logs were the only reliable signal in our test.
5. Practical rules
- Only hint resources the final page will actually use, and keep the same
as=type as the real tag, or the browser wastes a request. - Early Hints only helps when there is "think time" between the request and the 200. If your HTML is served instantly from cache, a 103 buys nothing.
- Headers that matter for security (
Content-Security-Policy, cookies) belong on the final response; a 103 is not a place to set them. - From the docs, not measured: many CDNs (for example Cloudflare) can generate 103s from the
Linkheaders of earlier final responses, so you may not need to change your origin at all. - Check what your server returns with the HTTP headers checker; for connection reuse behaviour see our keep-alive guide.
Frequently asked questions
What is HTTP 103 Early Hints?
An informational status the server sends before the final response, usually with Link preload or preconnect headers. In our Node.js 24 test the 103 arrived at 16 ms and the 200 at 1,013 ms on the same request.
Does Chrome support 103 Early Hints over HTTP/1.1?
No. Chromium 153 ignored our 103 on HTTP/1.1 and fetched the stylesheet only after the HTML arrived (1,018 ms). Over HTTP/2 it requested the stylesheet at 14 ms, about one second before the HTML.
How do I see a 103 response with curl?
Use curl -v. It prints the HTTP/1.1 103 Early Hints block with its Link header, then the 200 block. Both appear for a single request.
Why does fetch() not show the 103?
Node.js 24 fetch() skips 1xx responses and resolves only with the final 200. Use http.get and listen for the 'information' event to read the 103 and its headers.
How do I send Early Hints in Node.js?
Call res.writeEarlyHints({ link: '</style.css>; rel=preload; as=style' }) before res.writeHead. It worked unchanged on both http.createServer and http2.createSecureServer in Node.js 24.21.0.
Written with AI assistance. Measured on 2026-10-02 with Node.js 24.21.0 (http.createServer on localhost:8983, http2.createSecureServer with a self-signed cert on localhost:8984, response.writeEarlyHints, a 1,000 ms delay before the 200), curl 8.14.1, Node's http.get and built-in fetch, and headless Chromium 153.0.8010.12 (Playwright). We logged the time of every request the server received. Chromium's HTTP/2-only rule and the CDN notes are from the docs, not measured. PlainHeaders is our extension; Node.js, curl and Chromium are free.