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:

  1. Browser sends request → Server processes → Server sends headers + body

With Early Hints:

  1. Browser sends request → Server immediately sends 103 with Link headers
  2. Browser starts preloading resources referenced in those headers
  3. 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:

BrowserSupport
Chrome 103+Full support
Edge 103+Full support
Firefox 102+ (behind flag)Partial
Safari 16.4+Full support
Mobile ChromeFull 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.