Requirements
- nginx 1.25.0 or later with
http_v3_module. The packages from nginx.org include it.nginx -V 2>&1 | grep -o http_v3_moduleshows whether yours does. - TLS 1.3 must be enabled; QUIC is built on it.
- UDP port 443 must be reachable from outside, in addition to TCP 443.
With regular OpenSSL, nginx uses a compatibility layer for QUIC. It works but cannot do 0-RTT (early data). For most websites that does not matter.
The configuration
server {
listen 443 ssl;
listen [::]:443 ssl;
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
http2 on;
http3 on;
server_name 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;
# tell browsers that HTTP/3 is available
add_header Alt-Svc 'h3=":443"; ma=86400' always;
}
Important: reuseport may appear only once per address and port. With several server blocks, only the first gets listen 443 quic reuseport;; all others get just listen 443 quic;. Otherwise nginx refuses to start with “duplicate listen options”.
Then run nginx -t and systemctl reload nginx. The TLS configuration generator creates a complete, secure configuration including HSTS (option “HTTP/3”).
Open UDP 443
The most common reason HTTP/3 “doesn’t work”: the firewall only lets TCP through. Browsers then silently fall back to HTTP/2; there is no error message.
# ufw sudo ufw allow 443/udp # firewalld sudo firewall-cmd --permanent --add-port=443/udp && sudo firewall-cmd --reload # nftables (example) nft add rule inet filter input udp dport 443 accept
Also think of firewalls outside the server: cloud security groups, the hosting provider’s firewall or a load balancer in front. Many load balancers do not pass QUIC through.
How the browser switches to HTTP/3
On the first visit a browser connects over TCP (HTTP/2). The Alt-Svc header tells it that HTTP/3 is available on port 443, and it uses it from the next request on. ma=86400 means this information is valid for a day.
An HTTPS record in DNS is faster: the browser knows about HTTP/3 before the first connection.
example.com. 3600 IN HTTPS 1 . alpn="h3,h2"
A typical trap: add_header is not inherited once a location block has its own add_header lines, so Alt-Svc is missing exactly there. Solution: put the headers in a file and include it in every affected block.
Testing
# header present? curl -sI https://example.com | grep -i alt-svc # real HTTP/3 connection (needs curl with HTTP/3 support) curl -sI --http3-only https://example.com # is nginx listening on UDP? sudo ss -ulpn | grep ':443'
In the browser’s developer tools, the network view has a “Protocol” column: h3 means HTTP/3. On the very first request it still says h2; it switches after a reload.
The HTTP/2 & HTTP/3 check tests ALPN, Alt-Svc and the HTTPS record and sends a QUIC packet to the server.
FAQ
Does HTTP/3 really help?
Mainly on mobile and Wi-Fi networks with packet loss: a lost packet no longer blocks all requests, and connection setup is faster. On a fast wired network the difference is often hardly measurable.
Do I still need to offer HTTP/2?
Yes. Every browser starts over TCP, and some networks block UDP. HTTP/3 complements HTTP/2; it does not replace it.
Why does the browser show only h2 despite Alt-Svc?
Usually UDP 443 does not get through (firewall, load balancer, corporate network) or the browser does not have the information yet. Reload; otherwise test from outside with curl --http3-only.
Does HTTP/3 work with Apache?
No, Apache does not support HTTP/3 so far. A proxy in front such as nginx, Caddy or a CDN is the usual approach.