
Setting up HTTP/2 for Nginx
In Racing Trim
Since its beginnings, the World Wide Web has primarily been based on two technologies: the Hypertext Markup Language (HTML) and the Hypertext Transfer Protocol (HTTP). HTML is by far the better known technology and the one with the more turbulent past. In addition to questionable technological overkill (e.g., the <blink>
tag), conflicts between implementers have been a constant sideshow to the language's evolution. Some further developments such as XHTML have proved to be a technological dead end, causing considerable additional work in quite a few projects.
HTTP/2 at a Glance
Compared with HTML, however, the further development of HTTP has taken place rather covertly and has not been determined by political motives or marketing measures to any great extent. As it has become clear that many websites need to be available 24/7 and that failures could quickly become very expensive – not just in the e-commerce sector – the focus has shifted to solid technology and extensive compatibility. Innovations are therefore slow to catch on and have undergone intensive testing.
Today, only about half of all websites support HTTP/2 [1], even though it was standardized in RFC 7540 [2] back in May 2015. In the case of the Nginx web server, the ngx_http_v2_module
module replaced its predecessor ngx_http_spdy_module
in September 2015 (Nginx 1.9.5). April 2016 saw the module enter the stable branch (Nginx 1.10.0) [3]. By the way, the development of the Nginx module was financed by Dropbox and Automattic (WordPress).
The big websites in particular were early adopters of HTTP/2, preferring it over HTTP/1.1. For them, features like superior compression or request multiplexing were attractive because they directly improved the user experience thanks to higher speed. Today, HTTP/2 for websites is no longer a unique selling point, but rather a must-have feature that many users expect, if even unknowingly. HTTP/2 is supported by almost all state-of-the-art browsers, including Internet Explorer 11, at least as of Windows 10 [4].
On the major league web servers, however, HTTP/2 has not replaced HTTP/1.1, although it is available in parallel, ensuring downward compatibility with clients that cannot handle HTTP/2 without additional configuration overhead. Another advantage for the system administrator is that important features of HTTP/1.1 remain unchanged in HTTP/2. For example, most HTTP headers and the well-known status codes (1xx, 2xx, …) are the same, as are the request methods (GET
, PUT
, etc.).
The bottom line is that it is high time for operators of websites that do not yet support HTTP/2 to make the jump. In this article, I show you how best to implement HTTP/2 with Nginx.
Nginx and HTTP/2
HTTP/2 is provided in Nginx by the ngx_http_v2_module
module mentioned earlier. You enable it for a server context with the http2
parameter in the listen
directive:
server { listen 443 ssl http2; }
This example reveals one important prerequisite for the use of HTTP/2: The use of Transport Layer Security (TLS), formerly known as Secure Sockets Layer (SSL), is mandatory. HTTP/2 can therefore only be used for encrypted connections between the web browser and the web server because most web browsers request HTTP/2 by Application Layer Protocol Negotiation (ALPN), which in turn is part of TLS. However, this requisite should not be a major hurdle, because an encrypted connection is one of the basic requirements for the secure operation of a web server today. After the launch of Let's Encrypt [5], the creation of a widely accepted TLS certificate has become affordable, even for small projects.
After successfully configuring TLS and adjusting the listen
directive, HTTP/2 is enabled, which could mean the work is done. Important HTTP/2 features such as header compression, request multiplexing, and request pipelining are immediately available without further configuration.
Whether further configuration options are necessary depends in practice on the web server's load. If a web server only needs to process a few access requests per unit of time, it can offer the benefits of HTTP/2 to users without any negative effect. As access increases, however, additional configuration steps may be required, and they differ from the optimizations known for HTTP/1.1.
Optimizing the Configuration
To find the optimal configuration, you must understand the differences in the way HTTP/2 and its predecessor protocols process requests. HTTP protocols before HTTP/2 (i.e., HTTP/1.0, HTTP/1.1) are basically based on the idea of the original HTTP protocol (HTTP/0.9), which assumes that a web browser establishes a dedicated TCP connection to the web server for each request (i.e., for an HTML file, an image, etc.), sends the request, receives the response, and then closes the TCP connection again. With the further development of HTML, however, several requests quickly became necessary to display a complete page in the browser – not only many images, but also additional resources like stylesheets, JavaScript files, tracking pixels, and much more.
To process this flood of requests as quickly as possible with as little latency as possible, web browsers open as many as eight parallel TCP connections to the web server. To reduce the number of TCP connections, HTTP/ 1.1 introduced persistent connections. Additionally, the protocol specifies that web browsers should not establish more than two parallel connections to a web server (but browser vendors tend not to adhere to this).
Nevertheless, it was primarily relevant for the administrator to optimize the configuration of a web server for the highest possible number of simultaneous connections. Moreover, special image domains were often introduced in collaboration with application development (images.<example.com>) to handle more connections, resulting in pressure on application development to reduce the number of resources required for a website. The result, among other things, was the use of bundling (e.g., with Webpack) and spriting (creating a collage of multiple images in a file that is cut up again in the web browser).
HTTP/2 introduces streams and multiplexing that significantly advance the concept of persistent connections. A single TCP connection between the web browser and the web server can now process any number of requests, even in parallel. The number of concurrent TCP connections is now more or less equal to the number of concurrent users and, therefore, significantly lower compared with previous protocol versions. At the same time, however, the average duration of a TCP connection increases and possibly its memory requirements, as well, because more data traffic is now handled over this one connection.
Important Configuration Options
When configuring Nginx, two HTTP/ 2-specific configuration directives come to the fore that let you customize the web server to suit your application.
1. The http2_idle_timeout
directive specifies how long an HTTP/2 connection is kept open after the last data exchange. It is comparable to the older keepalive_timeout
directive for HTTP/1.1. Even the default values provided by Nginx – 75 seconds for keepalive_timeout
, 180 seconds for http2_idle_timeout
– show that different use cases have been considered. Although 75 seconds might be considered an average value for potential navigation by the user to the next web page, 180 seconds is more of a maximum value. These times are intended to ensure that any navigation by the user will be handled by the existing connection to the extent possible.
2. The http2_max_requests
directive specifies the maximum number of requests that can be processed over a given TCP connection. Again, it parallels HTTP/1.1 (keepalive_requests
), and again the default values (100 vs. 1,000) show that the optimization goal has changed significantly.
Both directives are necessary to protect the web server resources. The http2_idle_timeout
directive is intended to prevent concurrent connections to the web server from being blocked by clients that don't really need them. Because the amount of memory required per connection grows with the number of requests for that connection, http2_max_requests
is intended to prevent a connection from taking up too much memory for itself without actually needing it yet.
Characteristics of a Web App
In common, the two HTTP/2 directives are much more sensitive to the anatomy of the website or application compared with their older counterparts. To illustrate, I will draw on two (extreme) examples: the web version of RFC 7540 [2] and a web application such as Google Docs.
The RFC page is characterized by a minimal number of requests to display the page (the initial HTML document and a graphic as a favicon), a low probability of user navigation (because the page contains few links), and a long viewing time (because the document is very large and thus the reading time tends to be high).
Web applications, on the other hand, have completely different characteristics: Even the display of the first page requires dozens of requests. The multiple interaction options increase the likelihood that the user will navigate. Additionally, XHR requests can be triggered at any time by JavaScript. The user is likely to interact with the page for an extended period of time, which increases the probability that any of these interactions take place.
If you look at these extremes, you can see that correctly chosen configuration values become enormously important in HTTP/2. A very high value for http2_idle_timeout
is directly harmful in the case of RFC retrieval, because valuable connections remain unused. For the web application, on the other hand, a very high value directly improves the user experience, because the latency caused by the connection setup can be avoided in the best case.
A high value for http2_max_requests
is probably not harmful in the RFC example because the connection is very likely to be terminated before the maximum number of requests is reached. In the case of the web application, however, the advantages of HTTP/2 can only be exploited if the value is set high enough so that at least the initial page setup and the first user interactions can be handled completely after connection, so that no new connection setup is necessary.
These examples show two things: First, HTTP/2 can hardly realize its full potential if the administrator tends to be conservative in configuring it to average values. Second, the configuration is closely related to the application – not all websites are the same.
Moreover, you can also deduce that bundling and spriting have significantly less effect on the speed of a website. Therefore, a cost-benefit analysis with HTTP/2 looks different from that for HTTP/1.1. New projects, especially, might not need a bundling pipeline set up, and the graphics department can dispense with spriting.
Measure, Don't Guess
With HTTP/2, determining the appropriate configuration through measurements becomes more important. Because all modern browsers offer a more or less sophisticated developer console that allows deep insights into the anatomy of a website, metrics are not difficult to come by. In the following example, I use the Chrome developer tools (DevTools).
The Network tab (Figure 1) helps when trying to determine the number of requests for a common user interaction. Here, you select Preserve Log and Disable Cache and then use the search field to restrict the domain you want to examine (e.g., domain:www.linux-magazin.de). Now you load the website and make typical user actions. In the footer you can read the number of processed requests.

The Protocol column, for which you might need to right-click on the column headers, shows that the website is using HTTP/2 (h2). In the Connection ID column, you can then see the ID of the TCP connection, which you can use to discover when the browser establishes a new TCP connection.
The marketing department can give you an idea of how long users stay. The usual analysis tools (e.g., Google Analytics) provide good information in this regard. However, do not be deceived by low average values: You might need to look at the power user segment separately.
Freestyle: Server Push
HTTP/2 also offers another feature that significantly improves the user experience: server push, with which it is quite possible to predict which resources (images, JavaScript, etc.) the client will need, to display a web page fully.
To begin, it is enough to look into the header of an HTML file, which is usually a colorful mix of CSS and JavaScript resources. This information is also available to the web browser, but only after it has downloaded and parsed the initial HTML document. Only then does it send a request for the additional resources to the server and must wait for another network roundtrip before the responses arrive.
With server push in HTTP/2, you can send these additional resources directly after the response for the initial HTML file – before the browser even knows it will need them. The browser accepts the additional responses and stores them in the browser cache. If a request for a specific resource is then made during parsing, the response comes immediately and at high speed from the local cache; a network roundtrip to the server is therefore unnecessary.
A requirement of the resources that are to be sent by server push is that they must be bufferable. The browser must be able to read from the response headers, which work the same way for server push as for a normal request, in that the response will remain valid for a while. Nginx handles this requirement with the usual expires
configuration directive. Once set, you can use the http2_push
directive to push individual files to the client. In the example
location /index.html { http2_push /css/styles.css; http2_push /js/scripts.js; }
the styles.css
and scripts.js
files are sent directly with the response to a request for index.html
.
These actions can be tracked with Chrome DevTools by looking at the Initiator column. If Disable cache is enabled, the Push / … entry shows that the file arrived at the browser by HTTP/2 push. Be careful, though: Not all files the server has sent by push will appear here. If the current HTML page (so far) did not contain any request for a file, then it will not appear in the list. However, as soon as such a request is made, it appears there.
With Disable cache checked, however, the browser does not behave like a normal user. Alternatively, you can check the Nginx access log to see whether the server push is working correctly. Files sent by server push initially appear in the access log as normal requests, but if you add the keyword $msec
to the Nginx log output,
log_format http2 '$time_iso8601 $msec$status $connection $http2 "$request"';
you can see that all requests were processed at exactly the same time (Listing 1), which indicates a push.
Listing 1: Server Push in Log
2020-11-22T12:01:10+01:00 1606042870.567 200 605 h2 "GET /index.html HTTP/2.0" 2020-11-22T12:01:10+01:00 1606042870.567 200 605 h2 "GET /css/styles.css HTTP/2.0" 2020-11-22T12:01:10+01:00 1606042870.567 200 605 h2 "GET /js/scripts.js HTTP/2.0"
If you know the behavior of users on your own site well, you can consider pushing the most frequently visited follow-up page directly:
location /index.html { http2_push /css/styles.css; http2_push /js/scripts.js; http2_push /<next-page>.html; }
If the user follows this navigation path, the browser serves the corresponding request for /<next-page>.html
from the local cache with virtually no time delay.
Note, though, that Nginx does not remember which resources have been sent to a client and are most likely cached there. Therefore, configuration errors can cause Nginx to send resources that are already available to the client, wasting network bandwidth. Especially for resources with very different cache durations, you should pay very close attention to this possibility.
Nginx as Proxy
When Nginx works as a proxy in front of an application server, customizing all details of the web application in the HTTP/2 push configuration is not practical. At this point, the http2_push_preload
directive helps:
location /proxy/ { proxy_pass http://applicationserver/; http2_push_preload on; }
This directive processes link headers of upstream resources and sends resources marked with rel=preload
automatically (i.e., whenever a response from the application server sends this header along). For example, with the following statement, Nginx automatically sends the /css/styles.css
resource as a push to the client in addition to the response itself:
link: </css/styles.css>; rel=preload; as=style
This mechanism allows control of the push behavior directly from the web application. Because the web application probably has state management, it is also much easier to avoid unnecessary pushes at this point. For example, resources can only be sent by push at the start of a session.
Listing 2 shows an example Nginx configuration for HTTP/2 that can serve as a starting point for your own experiments.
Listing 2: Example Configuration
01 http { 02 03 log_format http2 '$time_iso8601 $msec $status $connection $http2 "$request"'; 04 05 server { 06 07 listen 443 ssl http2; # TLS and HTTP/2 08 09 http2_idle_timeout 3m; 10 http2_max_requests 1000; 11 12 access_log /usr/local/var/log/nginx/http2.log http2; 13 14 ssl_certificate /test.crt; 15 ssl_certificate_key /test.key; 16 17 root /var/www/html; 18 19 # Required for server push: 20 location /css/ { 21 expires 3h; 22 } 23 24 location /js/ { 25 expires 3h; 26 } 27 28 location /index-2.html { 29 expires 3h; 30 } 31 32 # Example for server push 33 # Call: GET / 34 location /index.html { 35 # These files are sent by push, 36 # when the client calls /index.html: 37 http2_push /css/styles.css; 38 http2_push /js/scripts.js; 39 # expected next navigation destination: 40 http2_push /index-2.html; 41 } 42 43 # Example of Nginx as proxy with http2_push_preload. 44 # The (simulated) application server is below. 45 # Call: GET /applicationserver/ 46 location /applicationserver/ { 47 proxy_pass http://localhost:1025/; 48 # Process link header with rel=preload: 49 http2_push_preload on; 50 } 51 } 52 53 # Simulation application server: 54 server { 55 list 1025; 56 root /var/www/html; 57 58 location ~ .html$ { 59 # Hints for http2_push_preload: 60 add_header link "</css/styles.css>; rel=preload; as=style"; 61 add_header link "</js/scripts.js>; rel=preload; as=script"; 62 } 63 } 64 }
Conclusion
Even without special configuration, enabling HTTP/2 brings a speed advantage to your website. With careful tuning of the maximum requests and idle times per connection, you can reduce the load on your server. Push mechanisms can also significantly improve the speed perceived by the user. In practice, implementing these optimizations will require the cooperation of the software development department, which in return, will reduce their efforts required for bundling and spriting.
Overall, the development of HTTP/2 clearly shows a considerable potential for optimization in closer coordination between software development and the web server infrastructure. With the release of the first draft of HTTP/3 in November 2020, it also became clear that such investments are worthwhile. Most likely, HTTP/3 will mainly optimize the multiplexing mechanisms introduced with HTTP/2 with respect to the underlying network layer. It is therefore safe to assume that, if HTTP/2 mechanisms are used sensibly, you will soon be able to benefit directly from the optimizations to be expected from HTTP/3. Therefore, the use of HTTP/2 already appears to be a worthwhile optimization that can be strongly recommended to every web server operator.