HTTP/2 can simultaneously send multiple requests and responses over a single TCP connection.

Let’s see it in action. Imagine a webpage with 20 small assets (images, CSS, JS). In HTTP/1.1, each asset would need its own TCP connection, leading to overhead and potential head-of-line blocking. With HTTP/2, all 20 assets can be requested and downloaded concurrently over one connection.

# Client initiates connection to server on port 443 (TLS required for HTTP/2)

# Client sends SETTINGS frame:
#   SETTINGS_MAX_CONCURRENT_STREAMS: 100 (client can handle 100 simultaneous streams)
#   SETTINGS_INITIAL_WINDOW_SIZE: 65535 (initial flow control window size)

# Server responds with SETTINGS frame:
#   SETTINGS_MAX_CONCURRENT_STREAMS: 100 (server can handle 100 simultaneous streams)
#   SETTINGS_INITIAL_WINDOW_SIZE: 65535 (server's initial flow control window size)

# Client sends HEADERS frame for Request 1 (GET /image1.jpg):
#   Stream ID: 1
#   Headers: :method=GET, :path=/image1.jpg, ...

# Server sends HEADERS frame for Response 1:
#   Stream ID: 1
#   Headers: :status=200, content-type=image/jpeg, ...

# Server sends DATA frame for Response 1 (image1.jpg content):
#   Stream ID: 1
#   Data: <binary image data>
#   Flow control: Server sends chunks, client ACKs by adjusting its window.

# Client sends HEADERS frame for Request 2 (GET /style.css):
#   Stream ID: 3
#   Headers: :method=GET, :path=/style.css, ...

# Server sends HEADERS frame for Response 2:
#   Stream ID: 3
#   Headers: :status=200, content-type=text/css, ...

# Server sends DATA frame for Response 2 (style.css content):
#   Stream ID: 3
#   Data: <binary css data>

# ...and so on for all 20 assets, multiplexed across streams 1, 3, 5, ...

The core problem HTTP/2 solves is the inefficiency of establishing and managing numerous TCP connections for a single logical document. It achieves this through multiplexing (multiple requests/responses on one connection), stream prioritization (allowing clients to tell servers which resources are more important), header compression (HPACK, reducing overhead), and server push (allowing servers to send resources before the client explicitly asks for them).

Tuning for maximum throughput involves understanding and adjusting several key parameters. The most impactful are SETTINGS_MAX_CONCURRENT_STREAMS and SETTINGS_INITIAL_WINDOW_SIZE.

SETTINGS_MAX_CONCURRENT_STREAMS dictates how many requests can be "in flight" simultaneously on a single connection. A higher value allows more parallelism, which is great for high-latency networks or servers with many small, fast resources. However, setting it too high can overwhelm the server or client’s processing capacity, leading to increased latency or dropped connections. A good starting point for servers is often 100, while clients might default to 100 or more. On Nginx, this is controlled by http2_max_concurrent_streams 100; in your nginx.conf’s http or server block.

SETTINGS_INITIAL_WINDOW_SIZE is a flow control mechanism. It defines how much data can be sent on a stream before the sender waits for an acknowledgment (a WINDOW_UPDATE frame) from the receiver. The default is 64KB (65535 bytes). For high-bandwidth, high-latency connections (like those to distant users), increasing this window size is crucial. It allows the sender to fill the "pipe" without waiting for acknowledgments, significantly boosting throughput. For example, if you have a 100ms RTT and a 10MB/s link, a 64KB window will limit your throughput to about 10MB/s * (64KB / (100ms * 10MB/s)) = 6.4 MB/s. Doubling the window to 128KB would double that. On Nginx, this is http2_initial_window_size 131072; (128KB). You’ll want to tune this based on your network conditions and the typical size of your responses.

SETTINGS_HEADER_TABLE_SIZE affects HPACK compression. It defines the maximum size of the HPACK dynamic table used to compress header fields. A larger table can achieve better compression ratios for repeated headers, but also consumes more memory. If the table grows too large, it can be pruned. A common tuning value is 65536; (64KB) for http2_header_table_size 65536;.

The specific values for these settings should be determined through load testing. Start with defaults, then incrementally increase SETTINGS_MAX_CONCURRENT_STREAMS and SETTINGS_INITIAL_WINDOW_SIZE while monitoring latency and error rates.

It’s a common misconception that HTTP/2 automatically solves all performance issues. While it’s a massive improvement over HTTP/1.1, misconfiguration or underlying network problems can still cripple performance. For instance, if your SETTINGS_INITIAL_WINDOW_SIZE is too small, you might have many concurrent streams (SETTINGS_MAX_CONCURRENT_STREAMS is high), but each stream will be sending data very slowly, bottlenecked by the tiny window. The system will appear to be doing a lot of work (many streams active), but actual data transfer will crawl.

The next performance hurdle you’ll encounter is optimizing the TLS handshake.

Want structured learning?

Take the full Performance course →