Early Hints and 103 Status Code Configuration
HTTP 103 Early Hints is a response status that lets servers send Link headers before the full response is ready, allowing browsers to preload critical resources while the server processes the request. This guide covers configuring Early Hints on Nginx, CDN support, preload/preconnect directives, and measuring the performance impact.
Prerequisites
- Nginx 1.13.9+ (for HTTP/2 support required by Early Hints)
- Linux (Ubuntu 20.04+/Debian 11+ or CentOS 8+/Rocky Linux 8+)
- HTTPS configured with HTTP/2 enabled
- Root or sudo access
Understanding HTTP 103 Early Hints
Normal HTTP request flow:
- Browser sends request → Server processes → Server sends headers + body
With Early Hints:
- Browser sends request → Server immediately sends 103 with
Linkheaders - Browser starts preloading resources referenced in those headers
- Server finishes processing → sends 200 with full headers + body
The result: critical CSS, fonts, and scripts start loading before the HTML arrives, improving LCP (Largest Contentful Paint) by hundreds of milliseconds on slow backends.
Supported Link types:
preload- fetch a resource immediately (high priority)preconnect- establish a connection to an origin early
Enabling Early Hints on Nginx
Early Hints in Nginx requires sending a 103 response before the proxied backend responds. This is done with the http2_push_preload directive or via the early_hints module:
# Check Nginx version supports it
nginx -V 2>&1 | grep -o 'version: nginx/[0-9.]*'
# Needs 1.13.9+ for HTTP/2, 1.25.1+ for native Early Hints
For Nginx 1.25.1+ with native Early Hints support:
server {
listen 443 ssl;
http2 on;
server_name www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/www.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.yourdomain.com/privkey.pem;
# Enable Early Hints
http2_early_hints on;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
# Add Early Hints headers for specific resources
add_header Link "</css/critical.css>; rel=preload; as=style" always;
add_header Link "</fonts/Inter.woff2>; rel=preload; as=font; crossorigin" always;
add_header Link "</js/app.js>; rel=preload; as=script" always;
add_header Link "https://fonts.googleapis.com; rel=preconnect" always;
}
}
For older Nginx using the header-based approach:
# Use a Lua block or map to send Early Hints via proxy
# This approach works with any Nginx version
location / {
# Send 103 Early Hints via a sub-request
proxy_pass http://127.0.0.1:8080;
# The proxy_set_header approach
proxy_set_header Early-Hints-Enabled "1";
}
Configuring Preload and Preconnect Directives
Choose resources carefully - preloading too many resources wastes bandwidth:
server {
# HTML pages get Early Hints for critical resources
location ~* \.html$ {
# Critical CSS (render-blocking)
add_header Link "</css/main.min.css>; rel=preload; as=style";
# Hero image (affects LCP)
add_header Link "</images/hero.webp>; rel=preload; as=image; imagesrcset='/images/hero-400.webp 400w, /images/hero-800.webp 800w'; imagesizes='(max-width: 400px) 400px, 800px'";
# Critical JavaScript
add_header Link "</js/app.min.js>; rel=preload; as=script";
# Web fonts
add_header Link "</fonts/Inter-Regular.woff2>; rel=preload; as=font; type=font/woff2; crossorigin";
# Third-party origins
add_header Link "https://www.google-analytics.com; rel=preconnect";
add_header Link "https://fonts.gstatic.com; rel=preconnect; crossorigin";
try_files $uri $uri/ /index.php?$query_string;
}
# API endpoints - no Early Hints needed
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
}
Use a map to serve different preload hints for different pages:
# In http {} block
map $uri $early_hint_resources {
"/" "</css/home.css>; rel=preload; as=style, </images/homepage-hero.webp>; rel=preload; as=image";
"~/product/" "</css/product.css>; rel=preload; as=style";
default "</css/main.css>; rel=preload; as=style";
}
server {
location / {
add_header Link $early_hint_resources;
proxy_pass http://127.0.0.1:8080;
}
}
Early Hints with PHP-FPM Backend
Application frameworks can send Early Hints programmatically:
<?php
// Send 103 Early Hints before any output
// Works in PHP 8.0+ with FastCGI
header_remove();
http_response_code(103);
header("Link: </css/main.css>; rel=preload; as=style");
header("Link: </js/app.js>; rel=preload; as=script");
// Force flush to send the 103 response
ob_flush();
flush();
// Now do the heavy processing
$data = fetch_from_database(); // This takes time
// Send the actual 200 response
http_response_code(200);
header("Content-Type: text/html");
echo render_page($data);
For Laravel:
// In a middleware
public function handle($request, Closure $next)
{
// Send Early Hints before controller runs
header("HTTP/2 103 Early Hints");
header("Link: </css/app.css>; rel=preload; as=style");
header("Link: </js/app.js>; rel=preload; as=script");
ob_flush();
flush();
return $next($request);
}
Nginx configuration for PHP with Early Hints:
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
# Allow PHP to send 1xx responses
fastcgi_keep_conn on;
}
CDN Support and Configuration
Cloudflare: Supports Early Hints natively (enabled by default for supported plans). Configure in the dashboard: Speed > Optimization > Early Hints.
Cloudflare Workers can add Early Hints:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
// Send Early Hints
const earlyHints = new Response(null, {
status: 103,
headers: {
'Link': '</css/main.css>; rel=preload; as=style',
}
});
// Send to client (not all environments support this)
const response = await fetch(request);
return response;
}
Fastly: Supports Early Hints via VCL:
sub vcl_deliver {
# Send 103 Early Hints (Fastly-specific)
if (fastly.ff.visits_this_service == 0) {
set resp.http.Link = "</css/main.css>; rel=preload; as=style";
}
}
AWS CloudFront: Does not natively support Early Hints as of 2024. Use Lambda@Edge or origin-level implementation.
Browser Compatibility
As of 2024, HTTP 103 Early Hints is supported in:
| Browser | Support |
|---|---|
| Chrome 103+ | Full support |
| Edge 103+ | Full support |
| Firefox 102+ (behind flag) | Partial |
| Safari 16.4+ | Full support |
| Mobile Chrome | Full support |
Check current support at caniuse.com/http103.
Browsers without Early Hints support simply ignore the 103 response and process the 200 response normally - it's fully backwards compatible.
Performance Testing
# Test if Early Hints is working with curl
curl -v --http2 https://www.yourdomain.com 2>&1 | grep -A5 "< HTTP/2 103"
# Expected output:
# < HTTP/2 103
# < link: </css/main.css>; rel=preload; as=style
# Measure improvement with WebPageTest CLI
npm install -g webpagetest
webpagetest test https://www.yourdomain.com \
--key YOUR_API_KEY \
--location "ec2-us-east-1" \
--runs 5 \
--first
# Use Chrome DevTools
# Open DevTools > Network tab
# Look for "103" status in the waterfall
# Compare preload start time vs document start time
# Lighthouse comparison
lighthouse https://www.yourdomain.com --only-audits lcp --output json | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(d['audits']['largest-contentful-paint']['displayValue'])"
Measure LCP improvement before/after:
# Quick TTFB test script
echo "Testing TTFB..."
for i in {1..10}; do
curl -o /dev/null -s -w "%{time_starttransfer}\n" \
--http2 https://www.yourdomain.com
done | awk '{sum+=$1; count++} END {print "Average TTFB:", sum/count, "seconds"}'
Troubleshooting
103 response not appearing:
# Verify HTTP/2 is enabled
curl -I --http2 https://www.yourdomain.com | head -1
# Should show: HTTP/2 200
# Check Nginx version
nginx -v # Need 1.25.1+ for native Early Hints
# Test with verbose curl
curl -v --http2 https://www.yourdomain.com 2>&1 | head -30
Resources not being preloaded in browser:
Open Chrome DevTools > Network and look for initiator "Other" or "Early Hints" on preloaded resources. If the Link header is present but resources aren't preloading, check that the as= attribute is correct.
Early Hints with HTTPS redirect issues:
Early Hints only works on HTTPS. Ensure http2_early_hints on is inside an SSL server block, not the HTTP one.
CDN stripping the 103 response:
Some CDNs strip 1xx responses. Test directly against your origin server to confirm Early Hints works, then configure CDN-specific settings.
Conclusion
HTTP 103 Early Hints delivers a meaningful LCP improvement for sites with slow application backends, essentially giving browsers a head start on loading critical resources while the server works. The investment is low — a few Nginx add_header Link directives — and the gains are proportional to your backend response time. Pair it with resource preloading for CSS, fonts, and hero images for the best results, and verify with Chrome DevTools that browsers are actually acting on the hints.


