How to Enable HTTP/2 in Nginx

How to Enable HTTP/2 in Nginx

How to Enable HTTP/2 in Nginx

The first time I benchmarked a site before and after switching on HTTP/2, I was surprised by how much of a difference it made on pages with dozens of small assets — CSS files, icons, fonts, tiny JS chunks. HTTP/2 fixes a lot of the inefficiencies baked into HTTP/1.1, and turning it on in Nginx takes about two minutes once you understand the one hard requirement it comes with: TLS.

In this article, I’ll explain what HTTP/2 actually changes under the hood, what you need before enabling it, how to configure it correctly, how to verify it’s working, and the performance and security considerations I always keep in mind when rolling it out.

What HTTP/2 Actually Changes

HTTP/1.1 opens a new TCP connection (or reuses a limited pool of them) for each set of requests, and browsers have historically capped concurrent connections per host at around six. That means a page with fifty assets ends up queuing requests behind each other, waiting for connections to free up. HTTP/2 solves this with:

The practical result: faster page loads, especially on latency-heavy connections and asset-heavy pages, with no changes needed to your actual application code.

Requirements

Checking If Your Nginx Build Supports HTTP/2

nginx -V 2>&1 | grep -o with-http_v2_module

If that returns with-http_v2_module, I’m good to go. If it returns nothing, I need to either install Nginx from the official Nginx repository (which bundles this module by default) or recompile from source with the flag included.

Checking Your Nginx Version

nginx -v

If I’m on anything older than 1.25.1, I should also be aware that older versions used a separate http2 parameter on the listen directive, while newer versions changed the syntax slightly — I’ll cover both below.

Step 1: Make Sure SSL Is Already Configured

I’m assuming SSL/TLS is already set up on the site (I have a dedicated article on enabling SSL and another on generating a self-signed certificate if you need to start from scratch). My server block already looks something like this before I touch HTTP/2:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com;
    index index.html;
}

Step 2: Enable HTTP/2

On Nginx 1.25.1 and Later (Recommended Syntax)

Starting with Nginx 1.25.1, the http2 directive is separated from the listen line into its own directive. This changed because the old syntax caused confusion when multiple listen directives were involved.

server {
    listen 443 ssl;
    http2 on;

    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com;
    index index.html;
}

On Nginx Versions Before 1.25.1 (Legacy Syntax)

Older versions use the http2 parameter directly on the listen line:

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

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com;
    index index.html;
}

I always check my exact version with nginx -v before deciding which syntax to use, since mixing them incorrectly on a newer version can throw a configuration error.

Step 3: Test and Reload

sudo nginx -t
sudo systemctl reload nginx

Complete Example Configuration

Here’s a full server block with HTTP/ 2, HTTP-to-HTTPS redirection, and reasonable SSL settings:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Verifying HTTP/2 Is Working

Using curl

curl -I --http2 -s https://example.com/ | head -1

If HTTP/2 is active, I see:

HTTP/2 200

If it falls back to HTTP/1.1, I get HTTP/1.1 200 OK instead, which tells me something in the config isn’t right.

Using Browser Developer Tools

I open Chrome DevTools → Network tab → right-click the column headers → enable the “Protocol” column. Requests served over HTTP/2 show h2; HTTP/1.1 requests show http/1.1.

Using an Online Checker

Tools like KeyCDN’s HTTP/2 Test or similar services will confirm protocol support from an external vantage point, which is useful for catching CDN or load balancer interference I might not see from curl on the origin server directly.

Troubleshooting Common Issues

nginx: [emerg] invalid parameter "http2" on listen. This means I’m running Nginx 1.25.1+ and trying to use the old syntax. Switch to the separate http2 on; directive.

unknown directive "http2" error. This usually means my Nginx build wasn’t compiled with --with-http_v2_module. I check with nginx -V as shown earlier, and if it’s missing, I install from the official Nginx APT/YUM repository instead of the distro’s default package.

Browser still shows HTTP/1.1 even after enabling HTTP/2. A few common causes:

Mixed content or broken assets after enabling HTTP/2. This is almost never actually caused by HTTP/2 itself — it’s usually a pre-existing hardcoded http:// reference in the site’s HTML/CSS that becomes more visible once HTTPS is strictly enforced.

Security Considerations

http2_max_concurrent_streams 128;

Performance Tips

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

Real-World Use Cases

Best Practices

Wrapping Up

Enabling HTTP/2 in Nginx is one of the highest-value, lowest-effort changes I make on almost every production site I manage — it’s usually a one-line addition once TLS is already in place. The protocol handles the heavy lifting of multiplexing and header compression on its own; my job is just making sure the certificate is valid, the syntax matches my Nginx version, and I’ve verified the upgrade actually took effect end-to-end, especially if there’s a CDN or load balancer in the request path.

Exit mobile version