Nginx HTTP/2 and HTTP/3 (QUIC): Configuration Guide for 2026

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

SymptomLikely causeFix
HTTP/3 not negotiatedUDP 443 blocked by firewallOpen UDP 443 in OS and cloud security group
nginx: [emerg] unknown directive "quic"Nginx built without --with-http_v3_moduleReinstall from official Nginx mainline repo
HTTP/2 works but HTTP/3 falls backTLS 1.3 not enabled or cert issueEnsure ssl_protocols TLSv1.3 is in effect
Only one worker handles QUICMissing reuseport on QUIC listenerAdd reuseport to both IPv4 and IPv6 quic listen lines
High connection reset rateMissing quic_retry onEnable 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 legacy listen ssl http2 syntax) for Nginx 1.25.1+.
  • Add listen 443 quic reuseport; and listen [::]:443 quic reuseport; for HTTP/3.
  • Open UDP port 443 in both the OS firewall and any cloud security group.
  • Add the Alt-Svc header so browsers discover and cache the H3 endpoint.
  • Enable quic_retry on to protect against UDP amplification attacks.
  • Test with curl --http3 and 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.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Please share this article on your favorite website or platform.