HTTP/2 has been the default for production Nginx deployments for years. HTTP/3, built on QUIC, is now a first-class Nginx feature as of version 1.25.0 — available in the official Nginx mainline and stable packages without custom builds. This guide covers enabling both protocols correctly, including TLS requirements, firewall rules, verification, and the gotchas that catch people out.
Prerequisites
- HTTP/2: Nginx 1.9.5+, any modern OpenSSL. Available in every current distro.
- HTTP/3: Nginx 1.25.0+ built with
--with-http_v3_module. The official Nginx mainline repo includes this by default from Ubuntu 22.04 / Debian 12 onwards. When installing from the official Nginx packages, QUIC TLS support is already included — the packages ship with a bundled QUIC-capable OpenSSL fork. OpenSSL 3.2+ or BoringSSL is only a requirement if you are compiling Nginx from source yourself.
Install from the official Nginx repo to guarantee HTTP/3 support:
# Ubuntu 22.04 / 24.04
curl -fsSL https://nginx.org/keys/nginx_signing.key \
| sudo gpg --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
https://nginx.org/packages/mainline/ubuntu $(lsb_release -cs) nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list
sudo apt update && sudo apt install nginx -y
# Verify HTTP/3 module is compiled in
nginx -V 2>&1 | grep http_v3
Verify Your Nginx Build
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'http_v[23]'
# Expected output:
# --with-http_v2_module
# --with-http_v3_module
HTTP/2: Enabling and Tuning
In Nginx 1.25.1+, HTTP/2 is enabled via the http2 directive (not the old listen 443 ssl http2 syntax). The old syntax still works for compatibility, but the new directive is cleaner:
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # New directive — Nginx 1.25.1+
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;
# HTTP/2 push (use sparingly — preload headers are often better)
# http2_push /static/main.css;
server_name example.com;
root /var/www/html;
index index.php;
}
HTTP/2 key settings:
http {
# Maximum concurrent streams per connection (default 128)
http2_max_concurrent_streams 128;
# Chunk size for HTTP/2 body (default 8k)
http2_chunk_size 8k;
# Connection idle timeout — aggressive for API servers
http2_idle_timeout 3m;
}
HTTP/3 (QUIC): Full Configuration
Add QUIC listeners alongside TCP. The reuseport flag is required when multiple worker processes are running — without it, only one worker handles all QUIC connections:
server {
# TCP listeners for HTTP/1.1 and HTTP/2
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
# UDP listeners for HTTP/3 (QUIC)
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# QUIC requires TLS 1.3 — 1.2 connections fall back to HTTP/2 or HTTP/1.1
ssl_protocols TLSv1.2 TLSv1.3;
ssl_early_data on; # 0-RTT — accept but protect against replay attacks
# Advertise HTTP/3 availability — browsers look for this header
add_header Alt-Svc 'h3=":443"; ma=86400' always;
# Optional: tell clients to remember the protocol upgrade for 24 hours
# ma=86400 means max-age 86400 seconds
server_name example.com;
root /var/www/html;
# Protect 0-RTT requests from replay (safe for GET, not POST)
location / {
if ($request_method = POST) {
set $early_data_reject $ssl_early_data;
}
proxy_set_header Early-Data $ssl_early_data;
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Firewall: Open UDP Port 443
QUIC runs over UDP. This is the most common oversight when setting up HTTP/3:
# ufw (Ubuntu)
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
# firewalld (RHEL/CentOS/Alma)
sudo firewall-cmd --permanent --add-port=443/udp
sudo firewall-cmd --reload
# iptables directly
sudo iptables -A INPUT -p udp --dport 443 -j ACCEPT
sudo ip6tables -A INPUT -p udp --dport 443 -j ACCEPT
If your server sits behind a cloud load balancer or NAT, also open UDP 443 in the provider's security group / firewall rules.
Full nginx.conf http Block Tuning
http {
# Keep-alive settings (benefit both HTTP/1.1 and HTTP/2)
keepalive_timeout 65;
keepalive_requests 1000;
# HTTP/2 settings
http2_max_concurrent_streams 256;
# QUIC/HTTP/3 settings
quic_retry on; # Requires stateless retry — mitigates amplification attacks
quic_gso on; # Generic Segmentation Offload — improves throughput on Linux
# SSL session cache (shared across workers)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
}
Verifying HTTP/2 and HTTP/3
# Check HTTP/2
curl -sI --http2 https://example.com | grep HTTP
# HTTP/2 200
# Check HTTP/3 (requires curl 7.88+ with nghttp3)
curl -sI --http3 https://example.com | grep HTTP
# HTTP/3 200
# Check Alt-Svc header (tells you HTTP/3 is advertised)
curl -sI https://example.com | grep alt-svc
# alt-svc: h3=":443"; ma=86400
# Online tools
# https://geekflare.com/tools/http3-test
# https://http3check.net
Drupal-Specific Considerations
Drupal's BigPipe module streams page chunks over a persistent connection, which benefits significantly from HTTP/2 multiplexing. No Drupal configuration changes are needed — BigPipe works transparently over HTTP/2.
If you run Varnish in front of Nginx, terminate HTTP/2 and HTTP/3 at Nginx, pass plain HTTP/1.1 to Varnish, and re-upgrade at the Nginx-to-client edge. Varnish does not support HTTP/2 natively as a backend protocol.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| HTTP/3 not negotiated | UDP 443 blocked by firewall | Open UDP 443 in OS and cloud security group |
nginx: [emerg] unknown directive "quic" | Nginx built without --with-http_v3_module | Reinstall from official Nginx mainline repo |
| HTTP/2 works but HTTP/3 falls back | TLS 1.3 not enabled or cert issue | Ensure ssl_protocols TLSv1.3 is in effect |
| Only one worker handles QUIC | Missing reuseport on QUIC listener | Add reuseport to both IPv4 and IPv6 quic listen lines |
| High connection reset rate | Missing quic_retry on | Enable retry to filter amplification traffic |
Summary Checklist
- Install Nginx from the official mainline repo to get HTTP/3 support.
- Use
http2 on;directive (not the legacylisten ssl http2syntax) for Nginx 1.25.1+. - Add
listen 443 quic reuseport;andlisten [::]:443 quic reuseport;for HTTP/3. - Open UDP port 443 in both the OS firewall and any cloud security group.
- Add the
Alt-Svcheader so browsers discover and cache the H3 endpoint. - Enable
quic_retry onto protect against UDP amplification attacks. - Test with
curl --http3and online validators before announcing. - Keep TLS 1.2 alongside TLS 1.3 — clients that cannot do QUIC fall back gracefully to HTTP/2 over TCP.