Enable HTTP/3 in nginx: step by step

HTTP/3 runs over QUIC and UDP instead of TCP. In nginx it takes only a few lines; most problems come from the firewall or a missing Alt-Svc header.

Requirements

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.