HTTP/2 doesn’t actually make your website faster by itself; it’s a set of instructions for how clients and servers should talk, and you have to tune it to get performance gains.

Let’s see HTTP/2 in action. Imagine a browser requesting a single HTML page with 50 small image assets. With HTTP/1.1, the browser would typically open 6-8 parallel TCP connections to the server. Each connection has overhead, and the browser has to wait for each connection to establish and then fetch assets one by one, even if they’re small. This leads to a lot of "head-of-line blocking" where one slow request can delay others.

Now, with HTTP/2, all those 50 image requests can be multiplexed over a single TCP connection. The browser sends a PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n client preface, and the server responds with its own preface. Once established, requests and responses are broken down into smaller frames. These frames, belonging to different requests, can be interleaved on the wire. The browser can receive image A’s data before image B’s data, even if image B was requested first, as long as image A’s data is ready. This dramatically reduces connection overhead and latency.

The core problem HTTP/2 solves is the inefficiency of multiple connections and head-of-line blocking inherent in HTTP/1.1. It achieves this through:

  • Multiplexing: Sending multiple requests and responses concurrently over a single TCP connection.
  • Header Compression (HPACK): Reducing the overhead of HTTP headers, which can be substantial for many small requests.
  • Server Push: Allowing the server to proactively send resources to the client that it anticipates the client will need, without the client explicitly asking for them.
  • Stream Prioritization: Enabling clients to tell the server which resources are more important, so the server can allocate resources accordingly.

To tune this, you’ll primarily be looking at your web server’s configuration. For Nginx, you’d enable HTTP/2 in your nginx.conf (or a site-specific conf file) like this:

server {
    listen 443 ssl http2;
    server_name example.com;
    # ... other SSL configuration ...
}

The http2 directive is the key here. For Apache, it’s similar:

<VirtualHost *:443>
    ServerName example.com
    Protocols h2 http/1.1
    # ... other SSL configuration ...
</VirtualHost>

The Protocols h2 http/1.1 line tells Apache to prefer HTTP/2 (h2) but fall back to HTTP/1.1 if the client doesn’t support it.

Beyond just enabling it, the real tuning comes from understanding how the underlying TCP connection behaves and how HTTP/2 frames interact.

One of the most impactful, yet often overlooked, parameters is the window_update mechanism in HTTP/2. Each TCP connection has a receive window size, and HTTP/2 adds its own flow control on top of that, applied per-stream and per-connection. If the receiver’s HTTP/2 window is too small, the sender will quickly fill it up and have to pause sending data, leading to stalled transfers. Conversely, a window that’s too large can lead to excessive memory usage on the receiver.

For Nginx, you can influence this indirectly via TCP tuning parameters on the server OS, and directly through http2_max_concurrent_streams and http2_max_concurrent_pushes. While these aren’t direct window size settings, they control the rate at which streams and pushes are created, which impacts window usage. For example:

http {
    # ...
    http2_max_concurrent_streams 1000;
    http2_max_concurrent_pushes 10;
    # ...
}

http2_max_concurrent_streams 1000 allows up to 1000 simultaneous streams on a single connection. http2_max_concurrent_pushes 10 limits how many resources the server can push at once. The optimal values depend heavily on your server’s capacity and the nature of your traffic. Too high, and you risk overwhelming the server or clients; too low, and you bottleneck throughput.

On the client side (which you can’t directly control but can influence by server behavior), browsers also have limits on concurrent streams per origin. The window_update frames are crucial for keeping data flowing. If you’re seeing stalled transfers or low throughput on high-latency connections, it’s often because the HTTP/2 flow control windows aren’t being managed effectively. The server needs to acknowledge received data by sending WINDOW_UPDATE frames back to the client, effectively increasing the window size for subsequent data. If your server OS or application is slow to process incoming data and send these acknowledgments, the HTTP/2 windows will shrink, and sending will pause.

The actual "tuning" often involves ensuring your operating system’s TCP receive buffer sizes are adequate, especially for servers. For Linux, you might adjust net.core.rmem_max and net.ipv4.tcp_rmem:

# Check current values
sysctl net.core.rmem_max
sysctl net.ipv4.tcp_rmem

# Example tuning (add to /etc/sysctl.conf and run sysctl -p)
net.core.rmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216

Setting net.ipv4.tcp_rmem to 4096 87380 16777216 means the initial, default, and maximum TCP receive buffer sizes are 4KB, 87KB, and 16MB respectively. This large maximum allows the TCP connection to buffer a significant amount of data, giving HTTP/2 more breathing room for its own flow control mechanisms and preventing sender stalls.

The one thing most people don’t realize is that HTTP/2’s stream prioritization is a hint, not a command. When a client sends PRIORITY frames, it’s telling the server, "Hey, I’d rather have resource X before resource Y." The server’s HTTP/2 implementation then uses this information to decide which data frames to send first from its buffers. However, if the data for resource Y is already buffered and ready to go, and the data for resource X isn’t, the server might still send Y first to maximize throughput and avoid wasting buffer space. It’s a complex interplay between what the client wants and what the server can deliver efficiently.

The next challenge you’ll encounter is managing the overhead of TLS handshake for an ever-increasing number of short-lived connections that HTTP/2 can enable.

Want structured learning?

Take the full Http2 course →