HTTP/2 Server Push lets your server proactively send resources to the client before the client even asks for them.

Let’s see it in action with a simple Go HTTP server.

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// Check if the client supports HTTP/2
		if r.ProtoMajor < 2 {
			log.Println("HTTP/1.1 request received, cannot use Server Push.")
			fmt.Fprintln(w, "Welcome! Please use an HTTP/2 capable client.")
			return
		}

		// Push the CSS file
		// The path here is relative to the server's root, not the client's current URL
		pushed, err := http.NewRequest("GET", "/style.css", nil)
		if err != nil {
			log.Printf("Error creating push request: %v", err)
		} else {
			// We need to use the ResponseWriter's Push method
			// The 'pushed' request needs to be passed to it
			w.(http.Pusher).Push(pushed)
			log.Println("Pushed /style.css")
		}

		// Now, serve the main HTML content
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		fmt.Fprintln(w, `<!DOCTYPE html>
<html>
<head>
    <title>HTTP/2 Server Push</title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>
    <h1>Hello from HTTP/2 Server Push!</h1>
    <p>If you see this with styling, Server Push likely worked.</p>
</body>
</html>`)
	})

	// Serve a dummy CSS file
	http.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) {
		log.Println("Serving /style.css")
		w.Header().Set("Content-Type", "text/css; charset=utf-8")
		fmt.Fprintln(w, `body { background-color: #f0f0f0; font-family: sans-serif; } h1 { color: navy; }`)
	})

	log.Println("Starting server on :8080")
	// Use http.ListenAndServeTLS for HTTP/2. You'll need a certificate and key.
	// For development, you can generate self-signed certs.
	// Example: openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes
	err := http.ListenAndServeTLS(":8080", "server.crt", "server.key", nil)
	if err != nil {
		log.Fatal("ListenAndServeTLS: ", err)
	}
}

To run this, you’ll need to generate self-signed certificates: openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes

Then, compile and run the Go program: go run main.go

Now, access https://localhost:8080 in a modern browser (like Chrome or Firefox) that supports HTTP/2 and has TLS enabled. You should see the page load with the CSS applied, and the browser’s network tab will show that style.css was "pushed" by the server, even though the HTML didn’t explicitly request it in a separate network round trip.

The core problem Server Push solves is reducing latency by eliminating the "request-response" cycle for critical, predictable resources. Imagine a user requesting your homepage. The browser receives the HTML, parses it, discovers it needs style.css, then makes a new request for style.css. This round trip, especially over high-latency networks, adds significant delay. Server Push allows the server to anticipate this need and send style.css as soon as it sends the HTML, often within the same TCP connection.

Internally, HTTP/2 uses multiplexing, meaning multiple requests and responses can be interleaved on a single TCP connection. Server Push leverages this. When the server decides to push a resource (like /style.css), it essentially creates a "virtual" request for that resource on the client’s behalf and then sends the response data for it. The client, upon receiving this pushed data, treats it as if it had initiated the request itself, associating the response with the correct resource path.

The key levers you control are:

  1. The http.Pusher interface: You cast the http.ResponseWriter to http.Pusher to access the Push() method. This method takes an *http.Request object representing the resource to be pushed.
  2. The resource path: The http.NewRequest("GET", "/style.css", nil) must use a path that your server can also handle normally. This is how the client associates the pushed data with the correct URL.
  3. Conditional pushing: In the example, r.ProtoMajor < 2 checks if the client actually supports HTTP/2. You should only attempt to push resources to HTTP/2 clients.
  4. When to push: This is the strategic part. You push resources that are critical for the initial render of the page and are highly predictable. Pushing too much, or resources that aren’t needed, wastes bandwidth and can actually harm performance.

The http.Pusher interface is a bit of a misnomer; it’s not just for pushing, but for initiating a server-initiated stream. When you call w.(http.Pusher).Push(pushed), the server isn’t just sending style.css data. It’s sending a PUSH_PROMISE frame to the client. This frame signals to the client, "Hey, I’m going to send you data for this URL (/style.css). You should prepare to receive it, and if you were about to ask for it, you can cancel your request." The server then immediately sends the actual style.css data in subsequent frames. This mechanism is crucial because it allows the client to avoid making its own redundant request.

A common misconception is that Server Push is a replacement for client-side resource discovery (like <link> tags). It’s not. It’s an optimization for predictable dependencies. The HTML still needs to contain the <link rel="stylesheet" href="/style.css"> tag so that the browser knows what to expect and how to associate the pushed data. If the HTML didn’t have the link, the browser wouldn’t know what to do with the pushed /style.css data. The PUSH_PROMISE tells the client to prepare for /style.css, and the subsequent <link> tag tells the client, "Okay, I need /style.css," and it can then immediately use the data that’s already arriving or has arrived.

The next thing you’ll likely run into is managing cacheability and invalidation for pushed resources.

Want structured learning?

Take the full Golang course →