HTTP/2 over TLS isn’t just a faster version of HTTP; it’s fundamentally a different protocol that happens to share the same port and often the same IP address.

Let’s see this in action. Imagine we’re debugging a client connection to example.com.

openssl s_client -connect example.com:443 -alpn h2

If this connection succeeds and you see HTTP/2 200 in the output, the TLS handshake completed with the Application-Layer Protocol Negotiation (ALPN) extension successfully indicating that both client and server support HTTP/2. If you see HTTP/1.1 200, ALPN failed to negotiate HTTP/2, and it fell back to HTTP/1.1. If it fails entirely, you’ll get an SSL handshake error.

The magic happens in the TLS handshake. When a client initiates a TLS connection to a server that supports HTTP/2, it includes an ALPN extension in its ClientHello message. This extension is a list of protocols the client supports. For HTTP/2, the identifier is h2. The server, if it also supports HTTP/2, will check this list and, if h2 is present, will respond with a ServerHello message that selects h2 from the client’s list. This selection signals to the client that HTTP/2 will be used over this TLS connection.

Here’s a simplified view of the handshake, focusing on ALPN:

  1. Client ClientHello: Contains a list of supported application protocols, e.g., [h2, http/1.1].
  2. Server ServerHello: If the server supports h2 and the client offered it, the server selects h2 from the client’s list. If not, it might select http/1.1 or fail the handshake.
  3. Application Data: Once the handshake is complete and h2 is negotiated, the client and server can start sending HTTP/2 frames.

The primary problem this solves is the "head-of-line blocking" issue inherent in HTTP/1.1. In HTTP/1.1, if you have multiple requests in flight, and one request takes a long time to process, all subsequent requests on the same connection are blocked, even if they are ready to be sent. HTTP/2, using multiplexing, allows multiple requests and responses to be sent concurrently over a single TCP connection. Each request/response pair is assigned a stream ID, and frames from different streams can be interleaved.

The key configuration points are on both the client and the server.

Client-side configuration (example: curl): Most modern curl versions enable HTTP/2 by default when connecting over TLS. To explicitly request it, you’d use:

curl --http2 https://example.com

If you want to force it or see if it’s being negotiated:

curl -v --http2 https://example.com 2>&1 | grep "HTTP/2"

Server-side configuration (example: Nginx): For Nginx, you need to enable http2 on the listen directive for your SSL-enabled server block:

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/your/cert.pem;
    ssl_certificate_key /path/to/your/key.pem;

    # ... other SSL settings ...

    location / {
        proxy_pass http://backend;
    }
}

Here, http2 appended to listen 443 ssl tells Nginx to advertise support for HTTP/2 via ALPN during the TLS handshake. Without it, Nginx would only offer http/1.1.

You can verify server support using openssl as shown earlier, or by using tools like nginx-http2-check:

nginx-http2-check example.com:443

This tool specifically probes the ALPN extension.

The most surprising thing is that HTTP/2 frames are not sent directly over TCP. They are first framed within TLS records, which are then sent over TCP. The TLS layer encrypts everything, and the ALPN extension is just a small part of the initial TLS handshake. After the handshake, the TLS layer is effectively transparent to the HTTP/2 protocol itself, but the performance benefits of HTTP/2 are realized within that encrypted tunnel. It’s not a separate protocol running alongside TLS; it’s a protocol inside TLS, chosen via TLS.

The next step is understanding how HTTP/2’s stream multiplexing and prioritization actually work at the frame level.

Want structured learning?

Take the full Http2 course →