Early Hints
103 Early Hints is an informational response code that allows the server to send a "pre-response" to the browser while it is still generating the main HTML.
Traditionally, when a user visits a website, a frustrating game of back-and-forth happens behind the scenes:
The Request: The browser asks the server for index.html.
The Server "Think" Time: The server receives the request, but it can't send the HTML immediately. It has to talk to databases, call APIs, and assemble the page. This often takes anywhere from 100ms to over a second.
The Idle Browser: During this time that the server is "thinking", the browser's network connection sits completely idle.
The Discovery Delay: Only after the server finishes and sends the HTML can the browser look inside it, discover the critical CSS and JavaScript files, and then start downloading them.
This creates a massive bottleneck where the user is staring at a blank screen while assets wait in a queue.
Your edge code can make use of 103 Early Hints to send instructions to the browser during this "idle browser" period. In effect, your code tells the browser that, while the server is still working on the HTML, the browser is going to need certain resources such as CSS, JS, and other files. The browser can then go ahead and start downloading them in the meantime.
How it works
In a standard HTTP setup, a server can only send one response header per request. However, the HTTP specification allows servers to send interim (temporary) responses before the final one, as long as they use a 1xx status code. Early Hints leverages this capability to send two sets of headers: an interim 103 response, followed by the final 200 response.
If you were to look at the literal bytes traveling over the network cable during that single request, it looks like this:
IMPORTANT: Fastly only supports 103 Early Hints over HTTP/2 or HTTP/3. While the examples below are shown in a text-based format for readability, HTTP/2 and HTTP/3 are binary protocols that transmit these messages as separate binary HEADERS frames.
The Client Request
The browser opens a connection and asks for the page.
GET /homepage HTTP/2Host: example.comThe Interim Response (The "Early Hint")
The server immediately sends back the 103 headers. Crucially, the server does not close the connection and does not send a body yet.
HTTP/2 103 Early HintsLink: </css/style.css>; rel=preload; as=styleLink: </js/app.js>; rel=preload; as=scriptNote the blank line at the end of the response here. Normally, this means "end of headers," and because it's a
1xxcode, the browser knows to keep listening for the next status code.The Final Response
While the browser is off downloading
main.cssandapp.js, the server finishes thinking and sends the final, official response down the exact same pipe:123456789HTTP/2 200 OKContent-Type: text/htmlContent-Length: 4096<!DOCTYPE html><html><head>...</head><body>...</body></html>Only now, after the 200 OK header and body are fully delivered, is the specific request-response transaction considered finished.
Implementing Early Hints in edge code
Fastly's platform has support for sending Early Hints in edge code. It is only effective for clients connecting using HTTP/2 or later.
- Fastly VCL
- Rust
- JavaScript
- Go
- C++
In a CDN service, call the early_hints function.
sub vcl_recv { if (fastly_info.is_h2 || fastly_info.is_h3) { early_hints("Link: </css/style.css>; rel=preload; as=style", "Link: </main/app.js>; rel=preload; as=script"); }}In a Compute application written in Rust, use the send_to_client() method of a Response instance with a 103 error code. Any number of 103 Early Hints responses may be sent before sending the final response. Any response with status code other than 103 will be treated as the final response.
123456789101112use fastly::{Error, Request, Response};#[fastly::main]fn main(req: Request) -> Result<Response, Error> {
let early_hint = Response::from_status(103) .with_header("Link", "</css/style.css>; rel=preload; as=style") .with_header("Link", "</main/app.js>; rel=preload; as=script");
early_hint.send_to_client();
Ok(req.send("example_backend")?)}In a Compute application written in JavaScript, use the sendEarlyHints() method of the FetchEvent object, passing in any initializer suitable for headers. This can be a simple object literal with string values; an array of name-value pairs, where each pair is a 2-element string array; or an existing Headers object.
Any number of 103 Early Hints responses may be sent before sending the final response.
123456789101112/// <reference types="@fastly/js-compute" />
addEventListener("fetch", event => event.respondWith(handler(event)));
function handler(event) { event.sendEarlyHints([ ['Link', '</css/style.css>; rel=preload; as=style'], ['Link', '</main/app.js>; rel=preload; as=script'], ]);
return fetch(event.request, { backend: "example_backend" });}In a Compute service written in Go, use the WriteHeader() function of the ResponseWriter interface, passing in a 103 status code. This will write all headers collected in the ResponseWriter to the response. It's important to call Reset() on the headers afterwards to clear out the entries sent as early hints.
1234567891011121314151617func main() { fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { w.Header().Add("Link", "</css/style.css>; rel=preload; as=style") w.Header().Add("Link", "</main/app.js>; rel=preload; as=script") w.WriteHeader(http.StatusEarlyHints)
resp, err := r.Send(ctx, "example_backend") if err != nil { fsthttp.Error(w, err.Error(), fsthttp.StatusBadGateway) return }
w.Header().Reset(resp.Header) w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) })}In a Compute application written in C++, use the send_to_client() method of a Response instance with a 103 error code. Any number of 103 Early Hints responses may be sent before sending the final response. Any response with status code other than 103 will be treated as the final response.
123456789int main() { auto early_hints_resp = fastly::Response::from_status(103) .with_header("Link", "</css/style.css>; rel=preload; as=style").value() .with_header("Link", "</main/app.js>; rel=preload; as=script").value(); early_hints_resp.send_to_client();
auto res = fastly::Request::from_client().send("example_backend").value(); res.send_to_client();}