./ahmedhashim

Early Hints

Modern browsers can start fetching critical assets before the final HTML arrives. The usual way to hint that work is with a Link header on the response, but if your backend takes any time to render the page, 103 Early Hints is a better approach.

It lets the server send preload and preconnect hints immediately, while the application is still doing the real work. Let’s look at how this works behind the Caddy reverse proxy.

A normal Link header on the final 200 OK response looks like this:

  sequenceDiagram
    participant B as Browser
    participant C as Caddy
    participant A as Backend

    B->>C: Request page
    C->>A: Forward request
    Note over A: DB queries, template rendering,<br/>other middleware work
    A-->>C: 200 OK + HTML
    C-->>B: 200 OK + Link headers
    Note over B: Browser sees Link headers,<br/>then starts CSS/JS/preconnect
    Note over B: Browser parses HTML and renders

This works, but the browser can’t act on those hints until the final response headers arrive. If your handler spends 50ms building the page, that entire 50ms is dead time from the browser’s perspective.

Early hints

With 103 Early Hints, the server can send Link headers before the final response is ready:

  sequenceDiagram
    participant B as Browser
    participant C as Caddy
    participant A as Backend

    B->>C: Request page
    C->>A: Forward request
    A-->>C: 103 Early Hints + Link headers
    C-->>B: 103 Early Hints + Link headers
    Note over B: Browser immediately starts CSS/JS preload<br/>and preconnect work
    Note over A: Backend continues DB queries,<br/>template rendering, etc.
    A-->>C: 200 OK + HTML
    C-->>B: 200 OK + HTML

The browser starts useful work immediately instead of waiting for the application to finish rendering.

Implementation

If you’re running behind Caddy, the early hints usually need to come from your application. Caddy will forward 103 responses, but it doesn’t generate early hints itself, so this is best handled in server middleware.

Here’s a simple example using the Go standard library:

func earlyHints(next http.Handler) http.Handler {
	v := getAssetsVersion()
	linkHeader := "<https://assets.example.com>; rel=preconnect, " +
		"<https://media.example.com>; rel=preconnect, " +
		"</app.css?v=" + v + ">; rel=preload; as=style, " +
		"</app.js?v=" + v + ">; rel=preload; as=script"

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if isAssetRequest(r.URL.Path) {
			next.ServeHTTP(w, r)
			return
		}

		w.Header().Set("Link", linkHeader)
		w.WriteHeader(http.StatusEarlyHints)
		next.ServeHTTP(w, r)
	})
}

The important part is that the middleware runs before the main handler does its expensive work. That gives the browser a chance to start preconnect and preload requests while the rest of the response is still being generated.

When deployed, this is what it looks like in the browser devtools network panel:

devtools

An added benefit of handling this at the application layer is that you can attach a version query parameter to static asset URLs based on the current build or release version.

Why it helps

The time saved is roughly equal to your backend response time.

If a page takes 50ms to produce, 103 Early Hints gives the browser a 50ms head start on:

  • preloading CSS and JavaScript
  • opening connections to asset or image domains
  • completing DNS, TCP, and TLS handshakes earlier

The biggest win is usually preconnect.

Connection setup alone can take tens of milliseconds, and sometimes much more on mobile or high-latency networks. If the browser can start DNS resolution and TLS negotiation while your backend is still rendering, those connections may already be warm by the time the HTML references images, fonts, or other subresources.

preload for CSS and JavaScript also benefits, especially on slower networks. On a fast local connection, a 50ms head start may be hard to notice. On real-world networks, it compounds with everything else and can reduce time to first paint.

The heavier the backend work, the more valuable early hints become. Pages with multiple database queries or expensive template rendering give the browser a larger window to make progress in parallel, turning what would otherwise be idle time into useful work.