HTTP/3 in Nginx is a version and build check before it is a configuration change. To enable HTTP/3 and QUIC in Nginx, the running binary must support the ngx_http_v3_module, the HTTPS virtual host must keep its normal TCP listener, and UDP 443 must be reachable from clients.
Older distro packages and custom source builds are the common traps. A server can serve HTTPS and HTTP/2 correctly while Nginx still rejects listen ... quic, or while browsers stay on HTTP/2 because the HTTP/3 listener is not advertised with Alt-Svc or UDP traffic is blocked.
Check Nginx HTTP/3 and QUIC Support
Start with the binary that is actually running on the server. Official Nginx documentation says QUIC and HTTP/3 support is available since Nginx 1.25.0 and is included in official Linux binary packages, while source builds need the --with-http_v3_module configure option.
| Requirement | Needed State | Why It Matters |
|---|---|---|
| Nginx version | Nginx 1.25.0 or newer | The quic listener parameter and ngx_http_v3_module started in 1.25.0. |
| HTTP/3 module | --with-http_v3_module appears in nginx -V | Source builds do not include the module by default, and some distro or custom packages may omit it. |
| TLS library | OpenSSL 1.1.1 or newer minimum; OpenSSL 3.5.1 or newer for 0-RTT with OpenSSL | HTTP/3 uses QUIC with TLS, and optional early data needs newer TLS support. |
| TLS protocol | TLSv1.3 available in the HTTPS server block | QUIC requires TLSv1.3, even when TCP fallback still allows TLSv1.2. |
| Network access | UDP 443 open in addition to TCP 443 | HTTP/3 runs over QUIC on UDP; normal HTTPS and HTTP/2 use TCP. |
Nginx still describes ngx_http_v3_module as experimental. Test the exact package or custom build on the target host before changing high-traffic virtual hosts, especially when a CDN, load balancer, or security appliance also handles HTTP/3.
Check the installed Nginx version first:
nginx -v
Example output from a compatible build:
nginx version: nginx/1.31.1
The version alone is not enough. Confirm that the running binary was built with HTTP/3 support:
nginx -V 2>&1 | tr ' ' '\n' | grep -- '--with-http_v3_module'
--with-http_v3_module
No output means the current Nginx binary is not ready for the configuration in this workflow. Upgrade to an HTTP/3-capable package or rebuild Nginx with --with-http_v3_module before adding any quic listener. If you need a custom build, use a maintained source-build workflow such as building Nginx from source on Ubuntu or building Nginx from source on Debian, then verify the final binary again with nginx -V.
Do not paste HTTP/3 directives into an older Nginx package and hope for graceful fallback. Unsupported builds can fail
nginx -tand block reloads for the whole service.
Enable HTTP/3 and QUIC in Nginx
Work from an HTTPS server block that already has valid certificate paths. HTTP/3 does not replace the normal HTTPS listener; it adds a UDP listener beside it. Set SITE_CONF to the active file for your site before backing it up. The example path uses a common conf.d layout; Debian and Ubuntu packages often use /etc/nginx/sites-available/, while source builds depend on the installed nginx.conf include path.
SITE_CONF=/etc/nginx/conf.d/example.com.conf
sudo test -f "$SITE_CONF" && \
sudo cp -a "$SITE_CONF" "$SITE_CONF.backup"
If the file check fails, correct SITE_CONF before continuing. Do not create a backup path for a guessed file; the later edit and rollback steps need the same active server-block file.
For a typical HTTPS virtual host on port 443, keep the existing TCP SSL listener and add a separate QUIC listener. Replace the certificate paths and server_name values with the real site values:
server {
listen 443 ssl;
listen [::]:443 ssl;
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
http2 on;
server_name example.com www.example.com;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
The TCP lines handle normal HTTPS, HTTP/1.1, and HTTP/2 fallback. The quic lines handle HTTP/3 over UDP. The Alt-Svc response header tells capable clients that the same origin is available over HTTP/3 on port 443.
If existing locations already use add_header, confirm Alt-Svc appears on the actual response path after reload. Standard Nginx header inheritance does not merge a higher-context header when the lower context defines its own add_header; on Nginx 1.29.3 or newer, add_header_inherit merge; can merge inherited headers after a successful syntax test.
Nginx defaults the http3 directive to on when the HTTP/3 module is available, so the separate QUIC listener is the critical configuration piece. Set http3 off; only when you intentionally want to disable HTTP/3 negotiation for a specific scope.
The http2 on; directive is the current Nginx syntax for HTTP/2 fallback and appeared in Nginx 1.25.1, but it is not an HTTP/3 requirement. If the site does not offer HTTP/2, or if the build reports unknown directive "http2", omit that line or keep the already working listen ... http2 syntax until your package provides the newer directive. Use set up HTTP/2 in Nginx when the fallback path needs its own setup and verification.
Omit the IPv6 [::] lines only when the host does not serve IPv6. If the site is dual stack, configure both IPv4 and IPv6 so HTTP/3 behavior matches normal HTTPS reachability.
Use reuseport Once Per QUIC Address and Port
reuseport lets multiple Nginx workers receive UDP packets on the same QUIC port, but Nginx accepts that socket option only once for each address and port pair. When several virtual hosts share the same IP and port, put reuseport on one listener and omit it from the others:
# First or default HTTPS virtual host owns the QUIC socket option.
server {
listen 443 ssl default_server;
listen 443 quic reuseport default_server;
server_name example.com;
}
# Additional HTTPS virtual hosts on the same address and port still enable QUIC.
server {
listen 443 ssl;
listen 443 quic;
server_name app.example.com;
}
If every server block repeats listen 443 quic reuseport;, sudo nginx -t can fail with a duplicate listener option error. For deeper listener behavior, see how to enable reuseport in Nginx.
Test and Reload Nginx
Validate the configuration before touching the running workers:
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload Nginx only after the syntax test passes:
sudo systemctl reload nginx
On non-systemd systems, use Nginx’s own reload signal instead:
sudo nginx -s reload
Open UDP 443 for QUIC Traffic
Opening HTTPS over TCP does not open QUIC over UDP. Prove the local Nginx listener first, then update the firewall layer that actually filters traffic for the server.
ss -H -ltn 'sport = :443'
ss -H -lun 'sport = :443'
Relevant output should show both TCP and UDP listeners. With reuseport and multiple workers, the UDP command can print more than one line for the same address and port:
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* UNCONN 0 0 0.0.0.0:443 0.0.0.0:*
Use only the firewall manager that is active on your server. For UFW-based systems, allow UDP 443 separately from any existing HTTPS TCP rule:
sudo ufw allow 443/udp
sudo ufw show added
sudo ufw status verbose
If UFW reports inactive, the rule is recorded but the firewall is not filtering traffic yet. Use ufw show added to confirm saved user rules on inactive hosts. Do not enable UFW on a remote server until the existing SSH access path is allowed and tested.
For Firewalld, identify the active zone before adding the rule. The default zone often owns the public interface, but the active-zone output is safer when interfaces or source bindings use a different zone:
sudo firewall-cmd --state
sudo firewall-cmd --get-active-zones
FW_ZONE=$(sudo firewall-cmd --get-default-zone)
If the interface serving the site appears under a different active zone, set FW_ZONE to that zone before adding, querying, or removing the UDP rule. Firewalld service definitions are protocol-specific, so do not assume an existing HTTPS TCP service also opens QUIC. Add the durable UDP rule explicitly and reload Firewalld:
sudo firewall-cmd --zone="$FW_ZONE" --add-port=443/udp --permanent
sudo firewall-cmd --reload
Confirm the UDP port rule is present after the reload:
sudo firewall-cmd --zone="$FW_ZONE" --query-port=443/udp
yes
SELinux and AppArmor are separate from the firewall rule. Keep SELinux enforcing on Fedora and RHEL-family systems; for standard UDP 443, test the Nginx listener and firewall path before changing policy. Investigate SELinux only when Nginx fails to start or logs an AVC such as a port-bind denial. On Ubuntu systems with AppArmor enabled, AppArmor does not open UDP ports; check it only when fresh denial logs name Nginx, a custom binary, or a nonstandard path.
Cloud security groups, provider firewalls, routers, load balancers, CDN settings, Docker port publishing, hosting panels, and raw nftables or iptables rulesets are separate control planes. If any of those sits in front of the host, allow UDP 443 there as well or let that edge layer own HTTP/3 instead. The firewall-cmd command examples cover Firewalld zone checks in more detail.
Verify HTTP/3 and QUIC from a Client
First confirm that the normal HTTPS response advertises HTTP/3. The curl command in Linux is enough for this header check because it does not need to use QUIC to read Alt-Svc:
curl -sS -D - -o /dev/null https://example.com | grep -iE '^(HTTP/|server:|alt-svc:)'
HTTP/2 200 server: nginx alt-svc: h3=":443"; ma=86400
That output proves the advertisement, not the QUIC path. For a real HTTP/3 probe, first check whether the curl build includes HTTP/3 support:
curl -V | grep -E '^Features:.*HTTP3'
A compatible curl build prints a Features: line containing HTTP3. The Protocols: line can still list only http and https because HTTP/3 uses normal HTTPS URLs. When HTTP/3 support is present, force HTTP/3 so the command cannot silently fall back to TCP:
curl --http3-only -sS -D - -o /dev/null https://example.com | grep -iE '^(HTTP/|server:)'
A successful response starts with an HTTP/3 status line:
HTTP/3 200 server: nginx
If your distribution’s curl does not include HTTP/3 support, use a different HTTP/3-capable client or check the browser’s network panel after a hard reload. Browser checks can be affected by cached Alt-Svc state, certificate trust, enterprise policies, VPNs, or a CDN that terminates HTTP/3 before traffic reaches the origin.
Tune Optional QUIC Settings Carefully
The basic listener and Alt-Svc header are enough for most deployments. Add optional QUIC directives only when they solve a specific operational problem:
| Directive | Default | Use It When |
|---|---|---|
quic_retry on; | off | You want QUIC address validation before accepting unauthenticated UDP traffic. It can add an extra QUIC retry for new clients. |
quic_gso on; | off | The Linux host and network interface support UDP segmentation offload, and testing shows it helps throughput. |
quic_host_key /path/to/key; | Random key on each reload | You want stateless reset and address-validation tokens to survive Nginx reloads. |
ssl_early_data on; | off | You deliberately want 0-RTT and can tolerate replay risk on the affected requests. |
Keep 0-RTT disabled unless the application can safely handle replayed early data. Nginx documents that 0-RTT with OpenSSL requires Nginx 1.29.1 or newer plus OpenSSL 3.5.1 or newer, while BoringSSL, LibreSSL, or QuicTLS builds can also support the module.
Disable HTTP/3 and Roll Back
To disable HTTP/3, remove the listen ... quic lines and any HTTP/3-only QUIC directives from the affected server blocks. Keep the normal listen 443 ssl; lines so HTTPS continues over TCP.
If clients may have cached the Alt-Svc advertisement, serve a temporary clear header before removing the header entirely:
add_header Alt-Svc 'clear' always;
Test and reload after the rollback edit:
sudo nginx -t
sudo systemctl reload nginx
Remove the local UDP firewall rule only if you added it for this Nginx HTTP/3 listener and no other service uses UDP 443. For UFW, remove the exact rule and review the remaining rules:
sudo ufw delete allow 443/udp
sudo ufw show added
sudo ufw status verbose
For Firewalld, use the same zone you used when adding the rule. The default-zone command is only a starting point; replace FW_ZONE when the active-zone output shows the site interface or source in another zone:
sudo firewall-cmd --get-active-zones
FW_ZONE=$(sudo firewall-cmd --get-default-zone)
sudo firewall-cmd --zone="$FW_ZONE" --remove-port=443/udp --permanent
sudo firewall-cmd --reload
sudo firewall-cmd --zone="$FW_ZONE" --query-port=443/udp
A no response confirms Firewalld no longer has the UDP 443 rule in that zone.
Troubleshoot Nginx HTTP/3 and QUIC
Nginx Rejects the quic Listener
Older or non-HTTP/3 builds usually fail the syntax test with a message that names quic or the missing HTTP/3 module:
sudo nginx -t
nginx: [emerg] invalid parameter "quic" nginx: configuration file /etc/nginx/nginx.conf test failed
Check the version and build flags again:
nginx -v
nginx -V 2>&1 | tr ' ' '\n' | grep -- '--with-http_v3_module'
If the module is missing, remove the HTTP/3 directives or switch to a package or source build that includes ngx_http_v3_module. Do not reload Nginx until sudo nginx -t passes.
Browsers Stay on HTTP/2
HTTP/2 fallback is normal for unsupported clients, first requests before the browser learns Alt-Svc, or paths where UDP is blocked. Separate those layers with focused checks:
curl -sS -D - -o /dev/null https://example.com | grep -i '^alt-svc:'
ss -H -lun 'sport = :443'
If the header is missing, the Nginx server block that handled the request does not include the Alt-Svc line, a lower add_header context replaced inherited headers, or another proxy layer removed it. If the UDP listener is missing, the listen ... quic line did not load. If both checks pass, inspect UFW, Firewalld, raw nftables or iptables rules, cloud firewalls, CDN HTTP/3 settings, VPNs, and the client itself. Check SELinux or AppArmor denial logs only when Nginx fails to start, bind, or read required files.
curl Does Not Support HTTP/3
Many Linux curl packages still ship without HTTP/3 support. Check the feature list before treating a failed --http3-only probe as a server problem:
curl -V | grep -E '^Features:.*HTTP3'
If the command prints no line, the local curl build cannot prove HTTP/3. Use another client, a container or package that includes HTTP/3-enabled curl, or a browser network panel that reports the negotiated protocol.
Nginx Reports Duplicate listen Options
Duplicate reuseport options on the same QUIC address and port can block reloads:
sudo nginx -t
nginx: [emerg] duplicate listen options for 0.0.0.0:443
Search the active configuration tree with grep, then keep reuseport on only one matching listener:
sudo grep -R --line-number -E '^[[:space:]]*listen[[:space:]].*(quic.*reuseport|reuseport.*quic)' /etc/nginx/
sudo nginx -t
HTTP/3 Works Through a CDN but Not on the Origin
A CDN, reverse proxy, load balancer, or hosting panel can terminate HTTP/3 at the edge and speak HTTP/2 or HTTP/1.1 to the origin. In that setup, browser HTTP/3 does not prove that origin Nginx is accepting QUIC. Test the origin directly only when DNS, firewall rules, SNI, and certificates allow a safe direct probe.
Set ORIGIN_IP to the origin address before running the probe. The --resolve option preserves the public hostname, HTTPS scheme, port, and SNI while bypassing normal DNS for that one request:
ORIGIN_IP=203.0.113.10
curl --noproxy '*' --resolve "example.com:443:$ORIGIN_IP" --http3-only -sS -D - -o /dev/null https://example.com | grep -iE '^(HTTP/|server:)'
Official Nginx and curl References
- Nginx QUIC and HTTP/3 support explains version availability, Linux package inclusion, source-build flags, and troubleshooting tips.
- Nginx ngx_http_v3_module documentation lists HTTP/3 directives, defaults, contexts, known issues, and the
$http3variable. - Nginx listen directive documentation defines the
quicparameter and socket-option rules for shared listeners. - Nginx ngx_http_v2_module documentation documents the current
http2directive and its Nginx 1.25.1 version note. - Nginx add_header documentation explains header contexts,
always, and inheritance behavior for headers such asAlt-Svc. - curl HTTP/3 documentation explains
--http3,--http3-only, and why HTTP/3 uses HTTPS URLs over QUIC.
Related Nginx Guides
HTTP/3 changes the client transport path, but other Nginx layers still matter after the listener works:
- Configure security headers in Nginx when you want consistent response headers across HTTPS virtual hosts.
- Change ports in Nginx if your site uses a nonstandard HTTPS port and needs matching listener and firewall checks.
- Use Nginx access and error logs when reloads fail, a virtual host handles the wrong site, or clients keep negotiating the wrong protocol.
- Create a reverse proxy in Nginx when HTTP/3 terminates at the edge but traffic still needs a clean backend handoff.
Conclusion
Nginx is ready for HTTP/3 when nginx -V shows --with-http_v3_module, the HTTPS server block has both TCP SSL and UDP QUIC listeners, Alt-Svc advertises h3, and a real HTTP/3 client reaches the site over UDP 443. Keep TCP HTTPS as the fallback path, and treat CDN or firewall layers as separate owners.


Formatting tips for your comment
You can use basic HTML to format your comment. Useful tags currently allowed in published comments:
<code>command</code>command<strong>bold</strong><em>italic</em><a href="https://example.com">link</a><blockquote>quote</blockquote>