HTTP/2’s SETTINGS frames are surprisingly complex, and most servers are deployed with default values that actively hinder performance.
Let’s see what SETTINGS frames look like in the wild. Here’s a trace from a real client connecting to a popular web server. We’ll focus on the SETTINGS frame sent by the server after the client sends its own.
Client: PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
Server: <SETTINGS FRAME>
Setting: SETTINGS_HEADER_TABLE_SIZE = 4096
Setting: SETTINGS_ENABLE_PUSH = 1
Setting: SETTINGS_MAX_CONCURRENT_STREAMS = 100
Setting: SETTINGS_INITIAL_WINDOW_SIZE = 65535
Setting: SETTINGS_MAX_FRAME_SIZE = 16384
This is the server’s initial configuration. It’s a starting point, but rarely optimal. The goal of tuning these settings is to maximize throughput and minimize latency by controlling how many streams can be active, how much data can be in flight, and how efficiently headers are compressed.
The Problem: Congestion and Head-of-Line Blocking
HTTP/2 multiplexes multiple requests over a single TCP connection. This is great for reducing connection overhead, but it introduces new challenges. If one stream is slow or blocked, it can still impact other streams on the same connection. The SETTINGS frame is how the server and client negotiate parameters to manage this.
The key parameters are:
SETTINGS_HEADER_TABLE_SIZE: Controls the size of the HPACK compression table. A larger table means more potential for efficient header compression, but also more memory usage and potential for state to get out of sync.SETTINGS_ENABLE_PUSH: Allows the server to push resources to the client that it anticipates the client will need. This can be a performance win, but if misconfigured or used aggressively, it can waste bandwidth.SETTINGS_MAX_CONCURRENT_STREAMS: The maximum number of simultaneously active streams the sender will allow. This is a crucial knob for controlling concurrency.SETTINGS_INITIAL_WINDOW_SIZE: The initial window size for flow control on each stream and on the connection as a whole. This dictates how much data can be sent before an acknowledgment (or aWINDOW_UPDATEframe) is received.SETTINGS_MAX_FRAME_SIZE: The maximum size of an individual HTTP/2 frame. Larger frames can be more efficient for bulk data transfer, but smaller frames can reduce latency for small transfers.
Tuning SETTINGS_MAX_CONCURRENT_STREAMS
The default SETTINGS_MAX_CONCURRENT_STREAMS is often 100. This is usually too low for modern servers handling many concurrent requests. If your client is limiting its concurrency based on this setting, you’ll see requests queue up.
Diagnosis: In your client’s HTTP/2 debugging logs, look for the server’s SETTINGS frame and note the value of SETTINGS_MAX_CONCURRENT_STREAMS. If this value is consistently lower than the number of requests you’re trying to make concurrently, it’s a bottleneck.
Fix: Increase SETTINGS_MAX_CONCURRENT_STREAMS. A common recommendation is to set this quite high, often to 2^31 - 1 (the maximum allowed value) on the client side to indicate no artificial limit, and a sufficiently large number on the server side. For Nginx, this is http2_max_concurrent_streams in the http or server block. For example:
http {
http2_max_concurrent_streams 1000; # Example value
# ... other http settings
}
Why it works: By allowing more concurrent streams, the server can handle more requests simultaneously without blocking. This is especially important for websites with many parallelizable resources (images, CSS, JS).
Tuning SETTINGS_INITIAL_WINDOW_SIZE
This is where things get tricky. SETTINGS_INITIAL_WINDOW_SIZE applies to each stream and the connection as a whole. The effective window size for a stream is the minimum of the connection window and the stream window. The default is 65535 bytes. This is often too small for efficient transfers of larger assets, leading to the sender waiting for WINDOW_UPDATE frames too frequently.
Diagnosis: Monitor your network traffic. If you see frequent WINDOW_UPDATE frames being sent by the client to the server, and the server’s data transfer rates are lower than expected, especially for larger files, the initial window size is likely too small. You can also look at the flowcontrol_window metric for specific streams in your HTTP/2 debugging tools.
Fix: Increase SETTINGS_INITIAL_WINDOW_SIZE significantly. A common modern recommendation is 2MB (2,097,152 bytes).
For Nginx, this is http2_initial_window_size in the http or server block. Note that Nginx’s http2_initial_window_size is set per stream, and the connection window is implicitly managed.
http {
http2_initial_window_size 2097152; # 2MB
# ... other http settings
}
Why it works: A larger window allows the server to send more data before needing a WINDOW_UPDATE. This reduces the number of round trips required for data transfer, especially for larger responses, leading to higher throughput.
Tuning SETTINGS_MAX_FRAME_SIZE
The default SETTINGS_MAX_FRAME_SIZE is 16384 bytes (16KB). Increasing this can improve efficiency for large data transfers by reducing the overhead of frame headers.
Diagnosis: This is harder to diagnose directly from typical logs. It’s more of a general tuning parameter. If you’re transferring large files and want to squeeze out every bit of performance, consider increasing it.
Fix: Increase SETTINGS_MAX_FRAME_SIZE. The maximum allowed is 16,777,215 bytes (16MB), but practical values are often around 1MB.
For Nginx, this is http2_max_frame_size in the http or server block.
http {
http2_max_frame_size 1048576; # 1MB
# ... other http settings
}
Why it works: Larger frames mean fewer frame headers per byte of data transferred, slightly reducing processing overhead and bandwidth usage for bulk data.
Tuning SETTINGS_HEADER_TABLE_SIZE
The default is usually 4096. Increasing this can improve compression ratios for headers, especially if you have many requests with repetitive header fields.
Diagnosis: If you’re seeing a lot of repetitive header data in your network traces, or if your HTTP/2 traffic is dominated by small requests with large headers, this might be a candidate for tuning.
Fix: Increase SETTINGS_HEADER_TABLE_SIZE. Values like 65536 are common.
For Nginx, this is http2_header_table_size in the http or server block.
http {
http2_header_table_size 65536; # 64KB
# ... other http settings
}
Why it works: A larger HPACK dynamic table allows more previously seen header fields to be stored and referenced by index, leading to smaller compressed header payloads.
SETTINGS_ENABLE_PUSH
The default is 1 (enabled). While push can be beneficial, it’s often a source of confusion and can be abused. Many modern applications disable it to gain more control over what’s sent.
Diagnosis: If you’re not using server push, or if you’re seeing unexpected resources being pushed, or if enabling push seems to degrade performance, it’s a candidate for disabling.
Fix: Set SETTINGS_ENABLE_PUSH to 0.
For Nginx, this is http2_push_preload off; or http2_push off; in the http or server block.
server {
# ...
http2_push off;
# ...
}
Why it works: Disabling server push prevents the server from sending unsolicited resources, simplifying traffic management and avoiding potential bandwidth waste if the client already has the resource or doesn’t need it.
The Next Headache: TCP Congestion Control
Once you’ve tuned your HTTP/2 SETTINGS, you’ll likely find that your TCP stack becomes the new bottleneck. This is where tuning TCP congestion control algorithms like BBR or Cubic becomes the next frontier.