HTTP/2 is a protocol that’s faster than HTTP/1.1, but it’s not magic; it’s a set of clever engineering tricks to overcome the inherent limitations of its predecessor.

Let’s see it in action. Imagine a simple web server serving a few static assets.

package main

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

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, HTTP/2!")
	})

	// To enable HTTP/2, you need to use TLS (HTTPS)
	// For local testing, you can generate a self-signed certificate
	// openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes
	log.Println("Starting server on :8443 with HTTP/2 enabled...")
	err := http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil)
	if err != nil {
		log.Fatal(err)
	}
}

When you run this and access https://localhost:8443 in a modern browser (which supports HTTP/2 over TLS), you’re already using HTTP/2. The browser and server negotiate the protocol. If the server supported HTTP/1.1 only, or if you were using plain HTTP (which most browsers block now), you’d fall back to HTTP/1.1. The key here is that the browser knows how to speak HTTP/2, and the server (Go’s net/http package with TLS configured) does too.

The fundamental problem HTTP/2 solves is head-of-line blocking. In HTTP/1.1, if you request multiple resources (like an HTML file, CSS, JavaScript, and images), they are sent sequentially over a single TCP connection. If one request gets stuck or takes a long time, all subsequent requests on that connection are also stalled, even if they are ready to be sent. Browsers tried to mitigate this by opening multiple TCP connections (typically 6-8 per host), but each connection has overhead (TCP handshake, slow start) and limits the number of concurrent requests.

HTTP/2 introduces multiplexing. Instead of sending whole requests and responses, it breaks them down into smaller, ordered binary frames. These frames from multiple requests can be interleaved on a single TCP connection. Imagine a train where each carriage is a frame. You can have carriages from different destinations (requests) all on the same train (connection), and they’re sorted out at the destination station. This means a slow response for one resource doesn’t block others.

Another significant change is server push. This allows the server to proactively send resources to the client that it anticipates the client will need, without the client explicitly asking for them. For example, when a client requests index.html, the server might also push style.css and script.js because it knows index.html will require them. This can reduce latency by eliminating the round trip time for the client to request those additional assets. You control this via the http.Pusher interface in Go’s http.ResponseWriter.

// Example of server push
func handler(w http.ResponseWriter, r *http.Request) {
	pusher, ok := w.(http.Pusher)
	if ok {
		// Push the CSS file
		err := pusher.Push("/style.css", nil)
		if err != nil {
			log.Printf("Failed to push style.css: %v", err)
		}
		// Push the JS file
		err = pusher.Push("/script.js", nil)
		if err != nil {
			log.Printf("Failed to push script.js: %v", err)
		}
	}
	// Serve the index.html file
	http.ServeFile(w, r, "index.html")
}

This is configured in the handler for the main resource. The server decides what to push.

HTTP/2 also introduces header compression using HPACK. In HTTP/1.1, headers are sent as plain text with every request, which can be repetitive and wasteful, especially with cookies. HPACK uses Huffman coding and a table of previously seen headers to encode headers much more efficiently, reducing the amount of data transmitted.

What stays the same? The core HTTP methods (GET, POST, etc.), status codes (200 OK, 404 Not Found), and the concept of URIs remain. The application-level semantics of HTTP are preserved. Your web application code that handles requests and generates responses doesn’t need to change fundamentally. The browser still sees a request for /resource and a response from the server. The underlying transport mechanism is just much more optimized.

The most surprising true thing about HTTP/2 is that it’s designed to be deployed over a single TCP connection per origin, which, counterintuitively, is more efficient than the multiple connections HTTP/1.1 used. The overhead of setting up and tearing down multiple TCP connections, along with the delays introduced by TCP’s slow start and congestion control on each one, is significant. By multiplexing many streams over one connection, HTTP/2 avoids these per-connection costs, leading to faster resource loading, especially on high-latency networks.

The next hurdle you’ll encounter is understanding how HTTP/3 builds upon HTTP/2’s foundations.

Want structured learning?

Take the full Http2 course →