March 24, 2017·4 min read·engineeringgohttpperformance

HTTP/2 push depends on predicting the next request

The Go HTTP/2 push guide explains synthetic requests, response ordering, and cache uncertainty. The API is small, but deciding what to push requires application knowledge.

A browser cannot request a stylesheet until it knows the stylesheet exists. When that knowledge arrives inside an HTML response, the connection may spend time waiting for discovery and another request. HTTP/2 server push lets the server act on knowledge it already has about the page. The difficult part is deciding whether that knowledge also describes what this particular client needs.

The March 24 guide by Jaana Burcu Dogan and Tom Bergan explains the support introduced in Go 1.8. The release notes place it alongside the other additions to net/http. Push is available when the running server and incoming connection use HTTP/2. It is an optional capability exposed by the response writer, so a handler must discover whether that capability exists.

I like this use of a small interface. The handler can express a useful optimization without requiring every response writer to support it. Ordinary request handling remains possible when the assertion fails, which keeps the optimization separate from the correctness of serving the page.

A promise represents a request

The protocol uses a PUSH_PROMISE frame to describe a request the server expects the browser to make. Once the browser receives the promise, it knows the resource is coming. If page processing subsequently discovers the same resource, the browser can wait for the pushed response instead of sending another request for it.

This makes push more specific than sending arbitrary extra bytes after a page. The server describes a request and produces a response associated with it. In Go, calling Push creates a synthetic request, describes it in the promise frame, and passes it through the servers handler to generate the response. Existing routing and response generation therefore remain relevant to pushed resources.

The guides basic pattern is a type assertion against http.Pusher. A handler can use it before writing its ordinary response.

if pusher, ok := w.(http.Pusher); ok {
    if err := pusher.Push("/app.js", nil); err != nil {
        log.Printf("push failed: %v", err)
    }
}

Here w is the handlers http.ResponseWriter. Failure to obtain a pusher leaves the handler free to send the HTML normally. A push error also needs a path that preserves normal page behavior. The browser must still be able to request the referenced script through the usual route.

The synthetic request means the target must have a working handler. Listing a script name in a push call does not itself supply the scripts content. The server still has to generate an appropriate response for that path, just as it would for a request initiated by the browser.

Ordering prevents duplicate work

The guide recommends initiating pushes before writing any response bytes. Its example explains why this ordering is part of correctness for the optimization. If the server first sends HTML containing a stylesheet link, the browser may discover the link and request the stylesheet before the push promise arrives.

The result can be two responses for the same stylesheet. The application has added bandwidth and server work while attempting to remove a round trip. Sending the promise before the HTML that reveals the dependency gives the browser the necessary information before it makes its own request.

This is a useful reason to decide on pushed dependencies early in the handler. A design that discovers its dependencies only after streaming most of the HTML may have already lost the opportunity. The relevant question is when the browser learns the dependency relative to when it receives the promise, not simply whether the server calls Push at some point.

Response variants need request information

A path alone may not identify the response the client needs. The guide uses Accept-Encoding to illustrate this. When a script response varies according to that header, the synthetic request should carry the corresponding value so the promise describes the right request variant.

Go exposes those headers through http.PushOptions. The guides example copies the incoming request header into the push options. This keeps the selected representation consistent with the browser request whose response the server is anticipating.

The mechanism also explains why push policy belongs close to application knowledge. A generic list of file paths may omit information used to select representations. The handler knows both the incoming request and the resources needed by its response, so it has the information necessary to form a more accurate prediction.

I regard that explicitness as valuable. An optimization that changes when requests happen still has to preserve which response the client receives. Keeping the target and relevant headers visible makes that requirement reviewable in ordinary Go code.

Authority and cache state constrain the prediction

The server can only push resources for which it is authoritative. The guide specifically excludes resources hosted by third-party servers or CDNs. Knowing that a page will use such a resource does not grant the current connection permission to provide its response.

Even an authoritative server may predict badly. The client may already have a cached copy, or it may never need a resource the server expected it to use. In those cases, sending the bytes consumes bandwidth without removing useful waiting. The guide warns that pushing every resource on a page can make performance worse.

The practical decision is therefore about a particular dependency and a particular opportunity to remove waiting. A resource needed immediately after the HTML is a different candidate from one used only after a later interaction. Redirects provide another opportunity identified in the guide, because following a redirect normally leaves a round trip in which the server already knows something about what comes next. The API supplies a way to act during that interval, while the application remains responsible for choosing the resource and its request headers.