Enabling HTTP/2 in NGINX: What Changes and What to Watch
Enabling HTTP/2 in NGINX is a one-line config change, but it shifts TLS negotiation, per-connection memory, and CPU profile. Here's the minimal setup, verification steps, and the trade-offs to monitor in production.
19 Oct 2025, 00:00 UTC

The Problem: HTTP/1.1 Bottlenecks on Modern Sites
If you serve a site with dozens of assets—CSS, JS, images, fonts—HTTP/1.1 forces browsers to open multiple TCP connections or queue requests on a single connection. That head-of-line blocking adds latency, especially on high-RTT links. NGINX has supported HTTP/2 since version 1.9.5, but flipping it on isn't just a one-line change; it shifts TLS handshake behavior, memory use per connection, and CPU profile. Here's what actually changes when you enable it, a minimal working config, and the trade-offs you'll hit in production.
What Enabling HTTP/2 Actually Does
Adding http2 to a listen directive (e.g., listen 443 ssl http2;) tells NGINX to advertise h2 via ALPN during the TLS handshake. Browsers that support HTTP/2 and TLS 1.2+ will negotiate it; older clients fall back to HTTP/1.1 automatically. No new worker processes spawn—HTTP/2 streams share the existing worker pool—but each connection now holds stream state, increasing per-connection memory.
Key behavioral changes:
- Multiplexing: Multiple request/response streams run over one TCP connection, eliminating head-of-line blocking at the HTTP layer.
- Header compression (HPACK): Request and response headers are compressed, reducing wire size but adding CPU overhead for encoding/decoding.
- Server push (optional): You can proactively push assets (
http2_push), though browser support is inconsistent and cache interaction is tricky.
Minimal Working Configuration
Assume you already terminate TLS on NGINX. The only required change is the listen line:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/certs/example.com.crt;
ssl_certificate_key /etc/nginx/certs/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Place this in /etc/nginx/sites-available/example.com (or your main nginx.conf), symlink to sites-enabled, then test and reload:
# Run as root or with sudo
nginx -t && systemctl reload nginx
Verification: Confirm the module is compiled in and the protocol negotiates:
# Check compile-time module
nginx -V 2>&1 | grep -o with-http_v2_module
# Expected: with-http_v2_module
# Live protocol check
curl -I --http2 https://example.com 2>&1 | head -1
# Expected: HTTP/2 200
If curl shows HTTP/1.1, either ALPN failed (check ssl_protocols includes TLSv1.2+) or the listen line is missing http2.
Trade-offs and Limits to Monitor
Memory per Connection
Each HTTP/2 connection maintains stream windows, HPACK dynamic tables, and flow-control state. On a busy site, this can push worker memory higher than HTTP/1.1. Monitor nginx -s status (or your metrics exporter) for active connections and reading/writing/waiting counts. If worker RSS grows unexpectedly, consider lowering http2_max_concurrent_streams (default 128) or http2_max_requests (default 1000) per connection.
CPU from HPACK
Header compression saves bytes but costs CPU. On small instances (e.g., 1 vCPU) serving thousands of RPS with large cookie headers, HPACK encoding can become a measurable fraction of CPU time. Profile with perf top -p $(pgrep -n nginx) if you suspect it.
Push Is Rarely Worth It
http2_push /style.css; in a location block pushes the asset before the browser asks. But browsers may already have it cached, wasting bandwidth. Most teams disable push (http2_push_preload off;) and rely on Link: </style.css>; rel=preload headers instead, letting the browser decide.
TLS Version Gate
Browsers require TLS 1.2+ for h2. If your ssl_protocols includes TLSv1 or TLSv1.1, those clients will negotiate HTTP/1.1. That's fine—just don't assume 100% HTTP/2 traffic.
Actionable Next Steps
- Add
http2to your existing TLSlistendirectives. - Run
nginx -tand reload. - Verify with
curl --http2 -I https://your-domain—look forHTTP/2 200. - Enable access-log
$http2variable (e.g.,log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http2"';) to track protocol distribution. - Watch worker memory and CPU for the first 24–48 hours. Adjust
http2_max_concurrent_streamsif RSS climbs.
HTTP/2 isn't a magic speed button, but for typical asset-heavy sites it reduces connection overhead and perceived latency with minimal config risk. The main operational cost is visibility—log the protocol, monitor memory, and you'll catch regressions early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.