My issues:
-
Enabling HTTP3/QUIC on my server causes long loading times (5000ms+ vs. ~30ms) and eventual fallback to HTTP2.
-
Enabling HTTP3/QUIC on subdomains results in HTTP 400 errors, and redirects to the apex domain.
How I encountered the problem:
I’m trying to enable HTTP3/QUIC on my webserver as a learning exercise. I started with selectively enabling HTTP3 on my main site, and didn’t have any issues, until enabling HTTP3 on one of my subdomains. The subdomain couldn’t be accessed over HTTP3 with either a web browser or curl, and eventually the subdomain seemingly started redirecting traffic to the apex domain. I’ve since disabled HTTP3 on my subdomains, but still have it enabled on the main site.
Solutions I’ve tried:
Updates, following NGINX Docs, following 3rd-party guides, only enabling HTTP3 on the main site.
Thank you for any suggestions or help.
Version of NGINX or NGINX adjacent software (e.g. NGINX Gateway Fabric):
nginx version: nginx/1.28.2
built with OpenSSL 3.6.1 27 Jan 2026
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --conf-path=/etc/nginx/nginx.conf --sbin-path=/usr/bin/nginx --modules-path=/usr/lib/nginx/modules --pid-path=/run/nginx.pid --lock-path=/run/lock/nginx.lock --user=http --group=http --http-log-path=/var/log/nginx/access.log --error-log-path=stderr --http-client-body-temp-path=/var/lib/nginx/client-body --http-proxy-temp-path=/var/lib/nginx/proxy --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-cc-opt='-march=x86-64 -mtune=generic -O2 -pipe -fno-plt -fexceptions -Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security -fstack-clash-protection -fcf-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -g -ffile-prefix-map=/build/nginx/src=/usr/src/debug/nginx -flto=auto' --with-ld-opt='-Wl,-O1 -Wl,--sort-common -Wl,--as-needed -Wl,-z,relro -Wl,-z,now -Wl,-z,pack-relative-relocs -flto=auto' --with-compat --with-debug --with-file-aio --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_degradation_module --with-http_flv_module --with-http_geoip_module=dynamic --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module=dynamic --with-http_mp4_module --with-http_perl_module=dynamic --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-http_v3_module --with-http_xslt_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre-jit --with-stream=dynamic --with-stream_geoip_module=dynamic --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-threads
Deployment environment:
Arch Linux via Pacman on a VPS
Minimal NGINX config to reproduce your issue (preferably running on https://tech-playground.com/playgrounds/nginx for ease of debugging, and if not as a code block): (Tip → Run nginx -T to print your entire NGINX config to your terminal.)
worker_processes 1;
# Load all installed modules
include modules.d/*.conf;
events {
worker_connections 1024;
}
http {
include sites-enabled/*;
include mime.types;
default_type application/octet-stream;
## Use strong cipher setup
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256;
ssl_conf_command Options PrioritizeChaCha;
ssl_protocols TLSv1.3 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
resolver 127.0.0.53;
# Clear unecessary information disclosure
server_tokens off;
more_clear_headers Server;
more_clear_headers X-Powered-By;
quic_host_key /etc/nginx/ssl/quic_host.key;
http3_stream_buffer_size 1m;
quic_retry on;
## TODO: Check NGINX docs on the danger of replay attacks
# ssl_early_data on;
## TODO: Check if my setup supports Generic Segmentation Offloads for QUIC/UDP
# quic_gso on;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
keepalive_timeout 65;
types_hash_max_size 4096;
types_hash_bucket_size 64;
gzip on;
gzip_min_length 7680;
gunzip on;
brotli on;
brotli_comp_level 5;
brotli_min_length 7680;
brotli_types text/plain text/css text/javascript application/javascript application/json image/svg+xml font/ttf;
http2 on;
log_format json_analytics escape=json '{'
'"msec": "$msec", ' # request unixtime in seconds with a milliseconds resolution
'"connection": "$connection", ' # connection serial number
'"connection_requests": "$connection_requests", ' # number of requests made in connection
'"pid": "$pid", ' # process pid
'"request_id": "$request_id", ' # the unique request id
'"request_length": "$request_length", ' # request length (including headers and body)
'"remote_addr": "$remote_addr", ' # client IP
'"remote_port": "$remote_port", ' # client port
'"time_local": "$time_local", '
'"time_iso8601": "$time_iso8601", ' # local time in the ISO 8601 standard format
'"request": "$request", ' # full path no arguments of the request
'"request_uri": "$request_uri", ' # full path and arguments if the request
'"args": "$args", ' # args
'"status": "$status", ' # response status code
'"body_bytes_sent": "$body_bytes_sent", ' # the number of body bytes exclude headers sent to a client
'"bytes_sent": "$bytes_sent", ' # the number of bytes sent to a client
'"http_referer": "$http_referer", ' # HTTP referer
'"http_user_agent": "$http_user_agent", ' # user agent
'"http_host": "$http_host", ' # the request Host: header
'"server_name": "$server_name", ' # the name of the vhost serving the request
'"request_time": "$request_time", ' # request processing time in seconds with msec resolution
'"upstream_connect_time": "$upstream_connect_time", ' # upstream handshake time incl. TLS
'"upstream_addr": "$upstream_addr", ' # upstream service address
'"ssl_protocol": "$ssl_protocol", ' # TLS protocol
'"ssl_cipher": "$ssl_cipher", ' # TLS cipher
'"ssl_curve": "$ssl_curve", ' # TLS KEX
'"scheme": "$scheme", ' # http or https
'"request_method": "$request_method", ' # request method
'"server_protocol": "$server_protocol", ' # request protocol
'}';
access_log /var/log/nginx/json_access.log json_analytics;
error_log /var/log/nginx/log_error.log;
error_log stderr emerg;
#geoip_country /usr/share/GeoIP/GeoIP.dat;
#geoip_city /usr/share/GeoIP/GeoIPCity.dat;
server {
listen 127.0.0.1:6767;
location /nginx_status {
stub_status;
allow 127.0.0.1; # only allow requests from localhost
deny all; # deny all other hosts
}
location /json_access.log {
root /var/log/nginx;
}
}
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
Main site (apex domain):
server {
listen 443 ssl;
listen [::]:443 ssl;
http3 on;
listen 443 quic reuseport default_server;
listen [::]:443 quic reuseport default_server;
server_name prusa.net;
include /etc/nginx/snippets/custom-404.conf;
ssl_certificate /etc/letsencrypt/live/prusa.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/prusa.net/privkey.pem;
# Set webroot
root /srv/prusa.net/public;
index index.html;
add_header alt-svc 'h3=":$server_port"; ma=3600';
add_header x-quic 'h3';
## WebSec headers
include /etc/nginx/snippets/headers-minimal.conf;
include /etc/nginx/snippets/block-ai-bots.conf;
add_header referrer-policy "strict-origin-when-cross-origin" always;
# CSP
add_header Content-Security-Policy "default-src 'none'; connect-src 'self'; img-src 'self'; media-src 'self'; font-src 'self'; style-src 'self' 'unsafe-inline'; form-action 'none'; frame-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; base-uri 'self'; manifest-src 'self'; script-src-elem 'self' 'sha256-5GTl1LT/HgujxC+LNqqToWLC0q3W4RbxbGdKE3tU5zc=' 'sha256-f4NreiI8uGE1wdwjfXuMtccvVCHbEOiN1QK+y2uSlrg=' 'sha256-59WfUf1GrPHL/ehnyY3hEqIIn5x7GqeH9vD2RCbB5zE=' 'sha256-xhi6ptLg8dTM2XBtV/SYNxtnsQEnSv5wPm9wxHSlpUI='" always;
}
# HSTS forward
server {
listen 80 default_server;
listen [::]:80 default_server;
return 301 https://$host$request_uri;
}
# Forward www to non-www
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.prusa.net;
ssl_certificate /etc/letsencrypt/live/prusa.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/prusa.net/privkey.pem;
return 301 $scheme://prusa.net$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name radek.prusa.net;
ssl_certificate /etc/letsencrypt/live/prusa.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/prusa.net/privkey.pem;
return 301 $scheme://prusa.net$request_uri;
}
Subdomain:
server {
listen 443 ssl;
listen [::]:443 ssl;
http3 on;
listen 443 quic;
listen [::]:443 quic;
server_name www.mon.prusa.net;
ssl_certificate /etc/letsencrypt/live/mon.prusa.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mon.prusa.net/privkey.pem;
return 301 $scheme://mon.prusa.net$request_uri;
}
# This is required to proxy Grafana Live WebSocket connections.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream grafana {
server localhost:3000;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name mon.prusa.net;
ssl_certificate /etc/letsencrypt/live/mon.prusa.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mon.prusa.net/privkey.pem;
## WebSec
include /etc/nginx/snippets/headers-minimal.conf;
include /etc/nginx/snippets/unified-securitytxt.conf;
include /etc/nginx/snippets/block-ai-bots.conf;
add_header Referrer-Policy "no-referrer" always;
proxy_hide_header X-Frame-Options;
proxy_hide_header X-XSS-Protection;
proxy_hide_header X-Content-Type-Options;
# CSP
# Grafana provides it's own CSP
# Block crawlers and indexers
add_header X-Robots-Tag "none" always;
add_header alt-svc 'h3=":$server_port"; ma=3600';
add_header x-quic 'h3';
location / {
proxy_set_header Host $host;
proxy_pass http://grafana;
}
# Proxy Grafana Live WebSocket connections.
location /api/live/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_pass http://grafana;
}
}