yum upgrades for production use, this is the repository for you.
Active subscription is required.
NGINX early hints support landed in NGINX 1.29.0 as the early_hints directive. Every stock Enterprise Linux, Debian and Ubuntu package still ships something older, so the directive is rejected outright. This article installs a current NGINX from GetPageSpeed, configures the safe HTTP/2 gate, and benchmarks what HTTP 103 actually does to LCP: 184 ms faster at the median, plus one configuration mistake that returns 502.
Short answer: yes, but only when your origin has real think-time. Across 60 interleaved cold-cache runs with a 400 ms origin delay, passing HTTP 103 cut LCP from 748 ms to 564 ms at the median and from 748 ms to 568 ms at p75. Repeat the same test against a zero-delay origin and the measured difference is exactly zero.
The catch is that early_hints requires NGINX 1.29.0 or newer, and no distribution ships that yet.
Install a current NGINX
RHEL / CentOS / AlmaLinux / Rocky Linux / Amazon Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx
That gives you nginx-1.30.4 on EL8, EL9 and EL10, verified at the time of writing. Check it:
nginx -v
# nginx version: nginx/1.30.4
Debian / Ubuntu
First, set up the GetPageSpeed APT repository, then:
sudo apt-get update
sudo apt-get install nginx
The APT repository carries 1.30.4 for Ubuntu jammy and noble, and for Debian bookworm.
Why your distribution’s NGINX will not do this
The early_hints directive appeared in NGINX 1.29.0. Every currently supported stock package predates it. These versions were read from the distributions’ own repositories:
| Distribution | Stock NGINX | early_hints |
|---|---|---|
| RHEL / Rocky / Alma 8 | module streams 1.14 to 1.24 | No |
| RHEL / Rocky / Alma 9 | 1.20.1 | No |
| RHEL / Rocky / Alma 10 | 1.26.3 | No |
| Debian 12 (bookworm) | 1.22.1 | No |
| Debian 13 (trixie) | 1.26.3 | No |
| Ubuntu 22.04 LTS | 1.18.0 | No |
| Ubuntu 24.04 LTS | 1.24.0 | No |
| Ubuntu 26.04 LTS | 1.28.3 | No |
| GetPageSpeed | 1.30.4 | Yes |
Even Ubuntu 26.04 LTS “Resolute Raccoon”, released in April 2026, stops at 1.28.3. On a clean Rocky Linux 10 box running its own nginx-1.26.3-6.el10_2.5, the directive fails the config test immediately:
nginx: [emerg] unknown directive "early_hints" in /etc/nginx/conf.d/eh.conf:9
nginx: configuration file /etc/nginx/nginx.conf test failed
After installing nginx-1.30.4-62.el10.gps from the GetPageSpeed repository, the same config file passes. That version difference is the entire prerequisite.
What NGINX early hints actually does
This is the part most write-ups get wrong, so it is worth stating plainly.
NGINX does not generate early hints. It has no directive that turns a list of assets into Link headers. What early_hints does is pass through a 103 Early Hints response that your upstream already produced, unmodified. The NGINX documentation is explicit: 103 responses received from an upstream server are passed to a client as is, without interpretation.
Two consequences follow:
- Your application has to emit the 103 itself. Node.js has had
response.writeEarlyHints()since v18.11.0, and Rails exposesrequest.send_early_hints. Elsewhere it may mean reaching for a lower-level API or a middleware. Either way it is opt-in work in your code, not a config flag in NGINX. - Only
proxy_passandgrpc_passforward early hints. In the NGINX source,pass_early_hintsis hardcoded to 1 in the proxy and gRPC modules only. FastCGI, uwsgi and SCGI upstreams do not forward interim responses, so a plain PHP-FPM site cannot use this without a proxy layer in front.
If your upstream sends no 103, adding early_hints changes nothing. That was verified directly: with the origin’s 103 suppressed, the client receives only the final 200 even with early_hints enabled.
Configuring NGINX early hints safely
The early_hints directive takes one or more values and passes the 103 when at least one of them is non-empty and not "0". It is valid in http, server and location contexts.
Do not enable it unconditionally. Interim responses are a compatibility hazard on HTTP/1.1, where intermediaries and older clients may mishandle them, and RFC 8297 recommends sending 103 only over HTTP/2 or later. Browsers act on early hints only for top-level navigations anyway, so hinting a fetch or XHR is wasted work.
This is the gate from the official documentation, and it handles both concerns at once:
map $http_sec_fetch_mode $early_hints {
navigate $http2$http3;
}
server {
listen 443 ssl;
http2 on;
location / {
early_hints $early_hints;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8090;
}
}
$http_sec_fetch_mode is navigate only for top-level document loads. $http2 and $http3 are empty on HTTP/1.1, so the concatenation is empty and the predicate is false. The result is that hints go out on HTTP/2 and HTTP/3 navigations, and nowhere else.
Verify it before you trust it
Run these against your own server. All four were confirmed on the Rocky Linux 10 test host.
An HTTP/2 navigation gets the 103 first, then the 200:
curl -k --http2 -H "Sec-Fetch-Mode: navigate" -o /dev/null -D- https://example.com/
HTTP/2 103
link: </assets/app.css>; rel=preload; as=style
link: </assets/hero.jpg>; rel=preload; as=image
HTTP/2 200
server: nginx/1.30.4
The same request over HTTP/1.1 must show no interim response at all:
curl -k --http1.1 -H "Sec-Fetch-Mode: navigate" -o /dev/null -D- https://example.com/
# HTTP/1.1 200 OK (and nothing before it)
A non-navigation request over HTTP/2 must also skip the hints:
curl -k --http2 -H "Sec-Fetch-Mode: cors" -o /dev/null -D- https://example.com/
# HTTP/2 200 (and nothing before it)
In Chrome DevTools, the confirmation is the initiator: a hinted resource shows an initiator type of early-hints rather than the HTML element that references it.
The benchmark
Configuration snippets prove syntax. They do not prove value. So the setup below measures it.
Method. The NGINX early hints benchmark isolates one variable. A fixed origin emits the same 103 with the same two Link headers, waits a fixed think-time, then returns the same HTML. Two NGINX server blocks are byte-identical except that one carries early_hints. The arm without it behaves exactly like stock NGINX: the 103 arrives and is dropped. Each run uses a fresh browser context with a cleared cache, so every run is cold.
Two design details matter more than the run count. First, the arms are interleaved: each round runs every arm once, in shuffled order, so that drift on the test host cannot land on one arm and masquerade as an effect. Second, there is an A/A control, a third arm pointing at the same server block as the “before” arm under a different label. The gap between those two identical arms is the noise floor, and it came out at 0 ms median and 4 ms at p75 across 20 rounds. Any claimed effect has to clear that.
Environment. Rocky Linux 10.1, NGINX 1.30.4 from GetPageSpeed, headless Chromium via Playwright 1.57, and a real 40 ms egress delay applied with tc qdisc ... netem delay 40ms for a measured RTT of about 42 ms. The hinted assets are a 610 byte render-blocking stylesheet and a 259 KB hero image, which is the LCP element. 20 rounds per condition.
Every run in the enabled arms recorded earlyHintsSeen: true and an image initiator of early-hints; every run in the disabled arms recorded neither. The arms are not mislabelled.
Origin think-time 400 ms:
| Metric | Stock behaviour | With early_hints |
Change |
|---|---|---|---|
| LCP median | 748 ms | 564 ms | -184 ms (-25%) |
| LCP p75 | 748 ms | 568 ms | -180 ms (-24%) |
| Hero image ready (median) | 736 ms | 531 ms | -205 ms |
| Stylesheet ready (median) | 572 ms | 531 ms | -41 ms |
| HTML arrived (median) | 527 ms | 527 ms | 0 ms |
| Raw TTFB (median) | 527 ms | 125 ms | -402 ms (artifact, see below) |
Control, origin think-time 0 ms:
| Metric | Stock behaviour | With early_hints |
Change |
|---|---|---|---|
| LCP median | 344 ms | 344 ms | 0 ms |
| LCP p75 | 344 ms | 344 ms | 0 ms |
| Hero image ready (median) | 334 ms | 333 ms | -1 ms |
The control is the important table, and it is worth being blunt about it: the benefit does not shrink, it disappears. Early hints buy the browser the use of your origin’s dead time. Remove the dead time and there is nothing left to reclaim.
That result is also a cautionary tale about method. An earlier block-ordered version of this benchmark, running all of one arm and then all of the other, reported a 44 ms gain in this same zero-delay control. Interleaving the arms and adding the A/A control showed that 44 ms was drift on the test host, not the feature.
Two results that are easy to misread
Early hints does not make your origin faster. Look at the “HTML arrived” row: 527 ms in both arms, identical to the millisecond. Nothing about the final response improved. The browser simply spent the waiting period fetching assets instead of idling.
Raw TTFB becomes a misleading metric. Navigation Timing responseStart fell from 527 ms to 125 ms once hints were enabled, an apparent 402 ms improvement, purely because the 103 is now the first response head to arrive. The HTML still showed up at 527 ms in both arms. That is a measurement artifact, not a speed-up, and it is more than twice the size of the real LCP gain. If you enable early_hints and your RUM dashboard reports a dramatic TTFB win, this is what you are looking at. Judge this feature on LCP, not on TTFB.
The mistake that returns 502
Early hints headers are read into the same buffer as the final response headers, and that buffer is proxy_buffer_size. A generously sized Link set overflows it.
With an origin emitting 200 Link headers and the default buffer, the whole request fails:
curl -k --http2 -H "Sec-Fetch-Mode: navigate" -o /dev/null -w "%{http_code}\n" https://example.com/
502
And in the error log:
upstream sent too big header while reading response header from upstream
The error names the response header, not the hints, which makes this genuinely confusing to diagnose. Raising the buffer fixes it:
location / {
early_hints $early_hints;
proxy_buffer_size 32k;
proxy_buffers 8 32k;
proxy_http_version 1.1;
proxy_pass http://127.0.0.1:8090;
}
The same request then returns the 103 and a 200. See our guide on tuning proxy_buffer_size in NGINX for how to size it properly.
There is a second, subtler limit inside NGINX: it tracks the cumulative size of early hints across a request and stops forwarding them once that total exceeds proxy_buffer_size, logging upstream sent too big early hints at info level. Since most deployments run error_log at warn or above, that message is invisible by default. If hints silently stop arriving, raise the log level before assuming the directive is broken.
Note also that proxy_hide_header applies to hint headers too, because the same hide-headers hash filters them.
What to hint, and what not to
The benchmark gain came from hinting exactly two things: the render-blocking stylesheet and the LCP image. Keep that discipline.
- Good candidates. The render-blocking CSS, the LCP image when it has a stable URL, a critical font, and
rel=preconnectfor a third-party origin you will certainly hit. - Bad candidates. Anything whose URL depends on the response you have not sent yet. If the hint is wrong, you have spent bandwidth and connection capacity on a file the page never uses, and on a constrained link that is a net loss.
- Responsive images. Browsers do not consistently preload
srcsetcandidates from an HTTPLinkheader, because the correct candidate depends on layout the browser has not computed. Hint a fixed URL or skip it. - Redirects. Send hints for the resources of the page you are actually going to serve. Hinting before a redirect wastes the fetch entirely.
- Cacheability. A returning visitor already has your CSS. Early hints helps first views far more than repeat views, so measure on cold cache or you will overstate it.
Keep the list short. Every hint competes for the same connection as the HTML you are about to send.
When to skip NGINX early hints entirely
- Your origin responds fast. The 0 ms control above shows why: no dead time, no gain whatsoever.
- You serve over HTTP/1.1 only. The recommended gate deliberately produces nothing there.
- Your upstream is FastCGI, uwsgi or SCGI. Interim responses are not forwarded for those.
- Your application cannot emit a 103 and you are not prepared to change it. NGINX will not invent one for you.
- Your preload targets are unstable. A wrong hint is worse than no hint.
If your stack does not clear that bar, spend the effort on the origin’s response time instead. That helps every metric, including this one.
Conclusion
NGINX early hints is a narrow tool with a real payoff inside its niche. Where an application server needs a few hundred milliseconds to produce HTML, handing the browser a correct Link set at the start of that window cut LCP by 184 ms at the median and 180 ms at p75 in a controlled, interleaved cold-cache benchmark whose noise floor measured 0 ms. Where the origin is already fast, the honest answer is that it does nothing at all: the zero-delay control moved LCP by exactly 0 ms.
The prerequisite is unavoidable: early_hints needs NGINX 1.29.0 or newer, and no stock Enterprise Linux, Debian or Ubuntu package ships that. The GetPageSpeed repository does, at 1.30.4, across EL8, EL9, EL10, Debian bookworm, and Ubuntu jammy and noble.
A directive this version-sensitive is one upgrade away from silently changing behaviour. A routine package update can turn the config you just benchmarked into a config that no longer does what you measured. GetPageSpeed Amplify runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics. Drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).
Get a current NGINX from the GetPageSpeed repository, which also carries 100+ dynamic modules for the same subscription. If you are weighing NGINX early hints against bundling your assets, our write-up on the NGINX concat module covers the other side of that trade.
