How to Enable HTTP/3 in Nginx

How to Enable HTTP/3 in Nginx

HTTP/3 has been the thing I’ve been most excited to roll out on my own servers over the past couple of years, and it’s finally reached the point where enabling it is genuinely practical rather than a bleeding-edge experiment. Unlike HTTP/2, which just changed how data was framed over an existing TCP connection, HTTP/3 replaces TCP entirely with QUIC, a transport protocol built on UDP. That’s a bigger architectural shift than it sounds, and it’s exactly why enabling it in Nginx isn’t quite as simple as flipping a flag — until relatively recently, it required a custom-compiled Nginx build.

Here’s the good news: as of Nginx 1.25.0, QUIC and HTTP/3 support is built into mainline Nginx, no third-party module or custom compile required (assuming your OS package or the official Nginx repo ships a build with --with-http_v3_module, which most current ones do). I’ll walk through the whole process, including how to check whether your installed Nginx even supports it, and what to do if it doesn’t.

What HTTP/3 Actually Changes

Quick primer, because understanding this helps you configure it correctly:

  • HTTP/1.1 — one request per connection at a time (unless pipelining, which is barely used).
  • HTTP/2 — multiplexes many requests over a single TCP connection, but a single lost packet stalls all streams on that connection (head-of-line blocking at the TCP layer).
  • HTTP/3 — runs over QUIC (UDP-based), where each stream is independent at the transport level. A lost packet only affects the specific stream it belonged to, not the whole connection. QUIC also has TLS 1.3 baked directly into the transport handshake, and it supports fast connection migration (useful for mobile clients switching between Wi-Fi and cellular).

The upshot: HTTP/3 is particularly beneficial for clients on lossy or high-latency networks — mobile users are the biggest beneficiaries.

Requirements

  • Nginx 1.25.0 or newer, compiled with --with-http_v3_module (check below how to confirm)
  • OpenSSL 3.0+ or BoringSSL/quictls (standard OpenSSL before 3.x doesn’t support QUIC’s TLS extensions properly — this trips a lot of people up)
  • A domain with a valid TLS certificate (HTTP/3 requires HTTPS — there’s no plaintext HTTP/3)
  • UDP port 443 open on your firewall, in addition to the usual TCP 443

Step 1: Check If Your Nginx Already Supports HTTP/3

nginx -V 2>&1 | grep -o 'with-http_v3_module'

If that prints with-http_v3_module, you’re set. If nothing prints, you’ll need to either install from the official Nginx repository (which ships QUIC-enabled builds) or compile from source.

Step 2: Install Nginx from the Official Repo (If Needed)

On Ubuntu/Debian:

sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring -y

curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
    | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://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

Using the mainline branch (rather than stable) is intentional here — HTTP/3 support matured fastest in mainline releases. Verify again:

nginx -V 2>&1 | grep -o 'with-http_v3_module'

Step 3: Confirm OpenSSL Compatibility

nginx -V 2>&1 | grep -o 'OpenSSL [0-9.]*'

If you see OpenSSL 3.x, you’re good — recent OpenSSL versions include the QUIC TLS extensions Nginx needs. If you’re stuck on an older OpenSSL and can’t upgrade the system package, the alternative is building Nginx against quictls, a QUIC-patched OpenSSL fork, but that’s a from-source build I’d only recommend if the package repo route isn’t an option for your OS.

Step 4: Get a TLS Certificate

If you don’t already have one:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot certonly --nginx -d example.com -d www.example.com

I’m using certonly here rather than the full --nginx auto-config flow, since we’re about to write a fairly specific HTTP/3-aware server block by hand.

Step 5: Configure Nginx for HTTP/3

sudo nano /etc/nginx/sites-available/example.com
server {
    listen 443 quic reuseport;
    listen 443 ssl;
    listen [::]:443 quic reuseport;
    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.3;

    # Advertise HTTP/3 support to clients via the Alt-Svc header
    add_header Alt-Svc 'h3=":443"; ma=86400';
    add_header X-QUIC "h3" always;

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

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

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

Let’s unpack the important pieces:

  • listen 443 quic reuseport; — this opens a UDP listener on port 443 for QUIC traffic. reuseport allows multiple worker processes to bind the same port efficiently, which matters a lot for QUIC’s performance since each worker gets its own socket rather than contending on one.
  • listen 443 ssl; — you still need the regular TCP/TLS listener too. HTTP/3 support is advertised to clients, but browsers always start with HTTP/1.1 or HTTP/2 over TCP first, then upgrade to HTTP/3 on subsequent requests once they see the Alt-Svc header. You cannot serve HTTP/3-only; TCP is your required fallback.
  • http2 on; — enable HTTP/2 for the TCP path (this is the modern syntax; older Nginx used listen 443 ssl http2; inline, which is now deprecated in favor of the separate directive).
  • Alt-Svc header — this is what actually tells browsers “hey, I support HTTP/3 on this port, feel free to use QUIC for future requests to this origin.” Without this header, browsers will never even attempt the upgrade.
  • ssl_protocols TLSv1.3; — QUIC mandates TLS 1.3; there’s no point advertising older protocol versions on this listener.

Step 6: Open the Firewall for UDP

This is the step people most often forget, because it’s easy to assume “port 443” only means TCP.

sudo ufw allow 443/udp
sudo ufw allow 443/tcp
sudo ufw allow 80/tcp

If you’re behind a cloud provider’s security groups (AWS, GCP, etc.), make sure UDP 443 is allowed there too — a local ufw rule won’t help if the cloud firewall drops the packets first.

Step 7: Test and Reload

sudo nginx -t
sudo systemctl reload nginx

Testing Your Setup

The most reliable test is curl with explicit HTTP/3 support (requires a curl build compiled with --with-http3, which isn’t universal — check with curl -V | grep HTTP3):

curl --http3 -I https://example.com

If your local curl doesn’t support HTTP/3, use an online checker instead — search for “HTTP/3 test” and use a reputable one, or check via your browser’s DevTools:

  1. Open Chrome DevTools → Network tab
  2. Right-click the column headers → enable the “Protocol” column
  3. Reload the page
  4. Look for h3 in the Protocol column for your requests

Firefox has similar DevTools support under the Network tab’s protocol column.

You can also verify the Alt-Svc header is being sent correctly:

curl -I https://example.com | grep -i alt-svc

Expect something like alt-svc: h3=":443"; ma=86400.

Troubleshooting Common Issues

Browser never upgrades to HTTP/3 — Give it a moment; browsers cache the Alt-Svc advertisement and use QUIC on the next connection, not necessarily the first one that sees the header. Also confirm UDP 443 isn’t blocked somewhere in the path (corporate networks and some ISPs block or throttle UDP more aggressively than TCP).

nginx: [emerg] unknown directive "listen ... quic" — Your Nginx binary wasn’t built with --with-http_v3_module. Revisit Step 1/2.

Certificate errors specific to QUIC — Double check OpenSSL version; QUIC’s TLS handshake genuinely needs OpenSSL 3.x’s QUIC-related APIs, and errors here often look like generic SSL handshake failures rather than anything obviously QUIC-related.

Works locally, fails from outside — Almost always a cloud/network firewall dropping UDP 443. Test with nc -u -zv your-server-ip 443 from an external machine, or check your cloud provider’s security group / firewall rules console directly.

High CPU on the QUIC listener — QUIC does more per-packet crypto work than plain TCP+TLS in some configurations. Make sure reuseport is set so load spreads across worker processes, and keep Nginx and OpenSSL updated, since QUIC performance optimizations land frequently in newer releases.

Security Considerations

  • QUIC mandates encryption — there’s no way to accidentally serve HTTP/3 over plaintext, which is a nice built-in safety property compared to HTTP/1 and HTTP/2.
  • UDP amplification risk — QUIC includes anti-amplification protections in the protocol itself, but keep your Nginx and OpenSSL versions current, since this is an area still receiving active security hardening.
  • Rate limit QUIC connections the same way you would TCP — limit_conn and limit_req zones still apply at the http level regardless of transport:
limit_conn_zone $binary_remote_addr zone=addr:10m;

location / {
    limit_conn addr 50;
}
  • Keep TLS 1.3 cipher suites current. Since QUIC forces TLS 1.3, you inherit TLS 1.3’s already-strong default cipher suite selection — there’s less manual cipher tuning needed compared to older TLS 1.2 configurations.

Performance Tips

  • reuseport is not optional for real workloads — without it, all QUIC packets funnel through a single worker’s socket, creating an unnecessary bottleneck.
  • Tune quic_gso if available in your build — Generic Segmentation Offload for QUIC reduces per-packet CPU overhead on Linux kernels that support it (5.x+).
  • Increase UDP buffer sizes at the OS level for high-throughput QUIC servers:
sudo sysctl -w net.core.rmem_max=2500000
sudo sysctl -w net.core.wmem_max=2500000

Make these persistent by adding them to /etc/sysctl.conf.

  • Monitor connection migration behavior if you’re serving a lot of mobile clients — QUIC’s ability to survive a network change (Wi-Fi to cellular) is one of its headline benefits, and it’s worth confirming in your logs/metrics that connections are surviving these transitions rather than dropping and reconnecting.

Real-World Use Cases

  • A media-heavy news site with a large mobile audience saw meaningfully fewer reported load failures on flaky mobile connections after enabling HTTP/3, since QUIC’s per-stream loss recovery avoided the head-of-line blocking that hurt HTTP/2 on lossy networks.
  • An API gateway serving mobile app clients benefited from QUIC’s 0-RTT connection resumption, cutting perceived latency for repeat clients reconnecting after being backgrounded.
  • A global SaaS product with users across regions with varying network quality used HTTP/3 alongside a CDN that also supported QUIC end-to-end, compounding the latency benefits.

Best Practices Recap

  • Always keep the TCP/TLS listener active alongside QUIC — HTTP/3 is additive, not a replacement.
  • Send the Alt-Svc header so browsers know to attempt the upgrade.
  • Open UDP 443 at both the OS firewall and any cloud-level security groups.
  • Use reuseport on the QUIC listener for proper multi-worker load distribution.
  • Stick to TLS 1.3 for the HTTP/3 listener, since it’s required anyway.
  • Verify real-world support using browser DevTools or an HTTP/3-capable curl build, not just config syntax checks.
  • Track Nginx mainline releases — QUIC/HTTP/3 support is still evolving faster than most other Nginx features, so staying current pays off.

HTTP/3 is one of those upgrades that’s mostly invisible to your users in the best possible way — pages just feel a little snappier, especially on mobile, without anyone needing to know why. Once the firewall and header pieces are in place, it’s a genuinely low-maintenance addition to a modern Nginx setup.

Total
1
Shares

Leave a Reply

Previous Post
How to Set Up Nginx with WebDAV

How to Set Up Nginx with WebDAV

Next Post
How to Set Up Nginx with Flask and uWSGI

How to Set Up Nginx with Flask and uWSGI

Related Posts