HTTP/3 mit QUIC in nginx: aktivieren, prüfen, Fallback sichern
Aufbauend auf Teil 1 (Zertifikate automatisch mit acme.sh) und Teil 2 (ECC-Zertifikate, zentraler Webroot, saubere TLS-Einstellungen) geht es hier um das Transportprotokoll darunter: HTTP/3 über QUIC. Es ist kein Schalter, den man aus Prinzip umlegt – sondern eine zweite Tür zum selben Dienst, die auf UDP läuft, andere Fehler macht als TCP und deshalb einen sauberen Fallback braucht.
Warum es HTTP/3 überhaupt gibt
HTTP/1.1 und HTTP/2 laufen beide über TCP. TCP garantiert die Reihenfolge aller Pakete – und genau das wird zum Problem, wenn ein Paket unterwegs verloren geht: Alles dahinter wartet, auch Datenströme, die mit dem Verlust nichts zu tun haben. HTTP/2 hat dieses „Head-of-Line-Blocking“ nur auf die Verbindungsebene verschoben, nicht beseitigt.
HTTP/3 setzt deshalb auf QUIC, ein eigenes Transportprotokoll auf Basis von UDP. Die Streams werden einzeln verwaltet, ein verlorenes Paket blockiert nur seinen eigenen Stream. Dazu kommen ein kürzerer Verbindungsaufbau (TLS 1.3 ist eingebaut, nicht aufgesetzt), Verbindungsmigration (Wechsel von WLAN zu Mobilfunk ohne Neuaufbau) und – bewusst abschaltbar – 0-RTT.
| Thema | HTTP/2 | HTTP/3 |
|---|---|---|
| Transport | TCP, verschlüsselt mit TLS 1.2 oder 1.3 | QUIC über UDP, immer mit TLS 1.3 |
| Port | 443/tcp | 443/udp (zusätzlich, nicht stattdessen) |
| Paketverlust | blockiert die ganze Verbindung | blockiert nur den betroffenen Stream |
| Verbindungsaufbau | TCP-Handshake + TLS-Handshake | ein Handshake (QUIC + TLS 1.3) |
| Netzwechsel (WLAN → LTE) | Verbindung bricht ab | Verbindung zieht mit (Connection Migration) |
| Erkennung | direkt | über den Header Alt-Svc |
Was dein nginx können muss
Zwei Dinge müssen zusammenpassen: Das Modul http_v3 und eine TLS-Bibliothek, die QUIC
spricht. Ob beides da ist, sagt der Build-Aufruf deines nginx:
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'nginx/|http_v3|http_v2|openssl'
Erwartet wird eine Version ab 1.25.0 – dort kam HTTP/3 offiziell dazu – und die Zeile
--with-http_v3_module. Fehlt sie, hilft kein Konfigurationstrick: Du brauchst ein anderes
Paket.
| Weg | Wann sinnvoll | Worauf achten |
|---|---|---|
| Paket der Distribution | Debian 13, CachyOS/Arch | Erst nginx -V prüfen; ob HTTP/3 einkompiliert wurde, entscheidet das Paket, nicht die Version. |
| Repository von nginx.org | Wenn das Distributionspaket zu alt oder ohne v3 gebaut ist | Offizielles Repo einbinden, Pakete pinnen – sonst mischt sich das Distro-Paket ein. |
| Docker-Image | nginx als Container | UDP-Port mappen (siehe Feuer-Abschnitt) und im Container dieselbe Prüfung nginx -V fahren. |
| Caddy statt nginx | Wer HTTP/3 ohne Build-Fragen will | Caddy spricht QUIC ab Werk, aber du verlässt die nginx-Konfiguration dieser Serie. |
nginx -t prüft die Syntax, aber nicht das Modul.
Ob http3 verstanden wird, siehst du sofort, wenn du die Direktive in eine Testdatei schreibst
und nginx -t erneut laufen lässt: unknown directive "http3" heißt: Modul fehlt.
UDP 443: die häufigste Fehlerquelle
HTTP/3 braucht UDP 443 – und in den meisten Setups ist UDP der Port, der nie geöffnet
wurde. Symptom: Die Konfiguration ist korrekt, nginx -t ist grün, und trotzdem bleibt der
Browser bei HTTP/2. Die Verbindung kommt einfach nie an.
# UFW (Debian)
sudo ufw allow 443/tcp
sudo ufw allow 443/udp # <-- entscheidend
# nftables (Beispiel, an die eigene Tabelle anpassen)
sudo nft add rule inet filter input udp dport 443 accept
# Docker: der UDP-Port muss separat veroeffentlicht werden
docker run -d -p 443:443/tcp -p 443:443/udp nginx
In docker-compose.yml entsprechend:
ports:
- "443:443/tcp"
- "443:443/udp" # ohne diese Zeile bleibt HTTP/3 im Container unsichtbar
| Stelle | Was dort blockieren kann |
|---|---|
| Host-Firewall | UFW/nftables/firewalld ohne UDP-Regel – der Klassiker. |
| Container-Netzwerk | Nur -p 443:443/tcp veröffentlicht, UDP fehlt. |
| Router / NAT | Keine UDP-Weiterleitung, oder CGNAT beim Provider – hier hilft nur ein anderer Anschluss. |
| Cloud-Firewall | Bei vServern oft eine zweite, separate UDP-Regel nötig (Security Group). |
| Client-Netz | Viele Firmen-VPNs und manche Provider blocken UDP 443 – dann kommt der Besucher per HTTP/2. Kein Fehler, sondern der Normalfall. |
quic_retry on; dazu (Adressvalidierung vor dem Handshake) und ein Blick auf die
Paketraten unter Last. Ohne Retry kann ein einzelnes gefälschtes Paket eine Antwort an eine fremde
Adresse auslösen – das ist genau die Amplification, die man nicht bauen will.
Der Serverblock
Der entscheidende Punkt vorweg: Es wird nichts ersetzt. Der bestehende
listen 443 ssl;-Block bleibt genau so, wie er in Teil 2 aussah – dazu kommt ein zweiter
Listener auf UDP, die Direktive http3 on; und der Alt-Svc-Header, der den
Browsern von HTTP/3 erzählt.
server {
# QUIC laeuft ueber UDP - eigener Listener, plus TCP wie bisher
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
http3 on; # ab nginx 1.25
server_name pdf.meine.domain;
ssl_certificate /etc/ssl/private/pdf.meine.domain_ecc/fullchain.cer;
ssl_certificate_key /etc/ssl/private/pdf.meine.domain_ecc/pdf.meine.domain.key;
ssl_protocols TLSv1.2 TLSv1.3; # HTTP/3 nutzt intern immer TLS 1.3
ssl_ecdh_curve X25519:prime256v1:secp384r1;
# Der Hinweis an den Browser: dieselbe Adresse gibt es auch per HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400' always;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Danach sudo nginx -t && sudo systemctl reload nginx. Ein Neustart ist nicht nötig –
der Reload öffnet den UDP-Socket mit.
ssl_ecdh_curve aus Teil 2 auch hier – die ausgehandelte Gruppe entscheidet der Client, wie
dort gemessen. Bei aktuellen Browsern landet HTTP/3 damit auf X25519.
Die Schrauben im Detail
Für einen funktionierenden Betrieb reichen die vier Zeilen aus dem Serverblock. Die folgenden Direktiven sind für Sonderfälle – und man sollte wissen, welche davon echte Auswirkungen haben.
| Direktive | Standard | Wann du sie anfassen solltest |
|---|---|---|
quic_retry on | aus | Immer, sobald der Server öffentlich erreichbar ist: Adressvalidierung per Retry-Token vor dem Handshake. |
quic_gso on | aus | Linux-Kernel ≥ 4.18 vorausgesetzt: schickt mehrere QUIC-Pakete in einem Syscall – deutlich weniger CPU pro Paket. |
quic_host_key | zufällig beim Start | Nur bei mehreren Instanzen hinter derselben Adresse: gleicher Schlüssel für Retry-Tokens, sonst weisen sich die Instanzen gegenseitig ab. |
http3_max_concurrent_streams | 128 | Viele parallele Anfragen pro Verbindung? Höher setzen. Speicher pro Client steigt mit. |
http3_stream_buffer_size | 64k | Nur bei Problemen mit großen Uploads über QUIC anpassen. |
ssl_early_data on | aus | 0-RTT: spart eine Runde, ist aber wiederholbar. Braucht Session-Tickets – siehe Abschnitt zu acme.sh. |
$http3 im Log | – | Für die Auswertung: enthält h3, wenn die Anfrage über QUIC kam, sonst einen Bindestrich. |
# Eigene Logdatei mit Protokoll-Kennzeichnung
log_format quic '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'proto=$server_protocol h3=$http3';
access_log /var/log/nginx/access-h3.log quic;
Prüfen in vier Schritten
Nach dem Reload lässt sich in wenigen Sekunden belegen, ob HTTP/3 wirklich ausgeliefert wird.
# 1) Hat nginx das Modul?
nginx -V 2>&1 | tr ' ' '\n' | grep with-http_v3_module
# 2) Antwortet der Dienst ueberaupt per HTTP/3?
# (curl muss mit HTTP/3 gebaut sein: curl -V | grep HTTP3)
curl --http3-only -sI https://pdf.meine.domain/ | head -n 1
# HTTP/3 200
# 3) Ist der Fallback-Hinweis gesetzt? Die Antwort kommt hier bewusst per HTTP/2
curl -sI --http2 https://pdf.meine.domain/ | grep -i alt-svc
# alt-svc: h3=":443"; ma=86400
# 4) Der Handshake selbst - ALPN und Protokollversion
openssl s_client -quic -connect pdf.meine.domain:443 -alpn h3 < /dev/null 2>/dev/null \
| grep -E 'Protocol|Cipher|ALPN'
# Protocol: QUICv1
# ALPN protocol: h3
# Cipher is TLS_AES_256_GCM_SHA384
Diese vier Ausgaben sind die ganze Wahrheit: Modul vorhanden, QUIC-Verbindung möglich,
Fallback angekündigt, ALPN verhandelt. Im Browser ergänzt die Entwicklerwerkzeuge-Ansicht den letzten
Beweis – im Reiter „Netzwerk“ steht unter Protokoll dann h3 statt h2.
tail -f /var/log/nginx/access.log zeigt dann
"GET / HTTP/3.0" – und daneben weiter "HEAD / HTTP/2.0" von Werkzeugen, die
kein QUIC sprechen. Genau so soll es aussehen.
Warum HTTP/2 bleiben muss
Alt-Svc ist keine Weiterleitung, sondern ein Angebot: „Dieselbe Ressource
gibt es auch unter dieser Adresse per HTTP/3.“ Der Browser entscheidet. Deshalb sind vier Dinge wichtig:
| Punkt | Warum |
|---|---|
always im Header | Ohne always fehlt der Hinweis bei 4xx/5xx-Antworten – dann lernt der Browser h3 nie kennen. |
ma richtig wählen | Die Merkzeit in Sekunden. 86400 (ein Tag) ist ein guter Wert; fällt h3 später aus, greift der Rückfall erst nach Ablauf. |
| HTTP/2 aktiv lassen | Jeder Client ohne QUIC, jedes VPN und jeder HTTP-Test läuft weiter über TCP. Schaltest du h2 ab, verlierst du genau diese Fälle. |
| Alt-Svc im Cache | Browser merken sich das Angebot. Beim Testen deshalb immer ein frisches Profil oder den Cache des Alt-Svc-Eintrags leeren. |
curl --http3-only
trennt beide Fälle sauber: Kommt die Antwort, ist der Server in Ordnung – und es liegt am Client-Netz.
Zusammenspiel mit acme.sh
Die gute Nachricht: Für HTTP/3 brauchst du kein eigenes Zertifikat. QUIC nutzt dasselbe
ssl_certificate wie der TCP-Listener – das ECC-Zertifikat aus Teil 2 gilt also für beide
Wege. Auch die Erneuerung ändert sich nicht: acme.sh stellt neu aus und ruft
--reloadcmd, nginx lädt neu, der UDP-Socket bekommt das neue Zertifikat mit.
Drei Punkte sind dennoch erwähnenswert:
- Kein Port 80 mehr im Spiel: Für die Ausstellung selbst ändert HTTP/3 nichts. Wer aber ohnehin auf die DNS-Challenge umsteigt, kann Port 80 ganz schließen – HTTP/3 braucht ihn nicht.
- TLS 1.3 ist Pflicht: Steht in
ssl_protocolsnurTLSv1.2, funktioniert HTTP/2 weiter, HTTP/3 aber nicht. Der Listener ist dann still tot – es gibt keine Fehlermeldung, nur kein h3. - 0-RTT und Session-Tickets: In Teil 2 haben wir
ssl_session_tickets offgesetzt – für Forward Secrecy. 0-RTT („ssl_early_data“) braucht aber genau diese Tickets, um eine frühere Sitzung fortzusetzen.
ssl_early_data on – dann kann der erste Datenblock wiederholt werden
(Replay), was für lesende Dienste unkritisch ist, bei Uploads und POSTs aber riskant. Wer 0-RTT nutzt,
sollte den Upstream mit proxy_set_header Early-Data $ssl_early_data; informieren und dort
nicht-idempotente Anfragen ablehnen.
Was es wirklich bringt – und was nicht
HTTP/3 ist kein allgemeiner Turbo. Die ehrliche Einordnung: Auf einer guten Verbindung mit niedriger Latenz misst man kaum einen Unterschied, weil der Flaschenhals dort die Serverantwort ist. Der Gewinn entsteht dort, wo Verbindungen schlecht sind – Mobilfunk, WLAN mit Verlusten, hohe Paketumlaufzeiten.
| Situation | Erwartung |
|---|---|
| Erster Aufruf über Mobilfunk | Kürzerer Aufbau, weil Verbindung und Verschlüsselung in einem Handshake entstehen. |
| Verluste im Netz | Deutlich ruhigeres Verhalten: Ein fehlendes Paket blockiert nicht mehr alles. |
| Netzwechsel unterwegs | Verbindung bleibt bestehen, statt neu aufzubauen. |
| Lokales LAN, schnelles WLAN | Praktisch kein Unterschied – hier lohnt der Aufwand nur aus Neugier. |
| Viele kleine Requests über einen Proxy | Kann CPU kosten. Gegenmittel: quic_gso on und größere UDP-Puffer. |
# Latenz-Vergleich derselben URL, einmal h2, einmal h3-only
curl -o /dev/null -s -w 'h2: %{http_version} appconnect=%{time_appconnect}s ttfb=%{time_starttransfer}s\n' \
--http2 https://pdf.meine.domain/
curl -o /dev/null -s -w 'h3: %{http_version} appconnect=%{time_appconnect}s ttfb=%{time_starttransfer}s\n' \
--http3-only https://pdf.meine.domain/
# Bei viel Durchsatz: UDP-Puffer des Kernels anheben (Werte in /etc/sysctl.d/)
# net.core.rmem_max = 26214400
# net.core.wmem_max = 26214400
Wenn es schiefgeht
| Symptom | Ursache | Vorgehen |
|---|---|---|
nginx startet nicht: unknown directive "http3" | Modul http_v3 fehlt im Paket. | nginx -V prüfen, anderes Paket oder nginx.org-Repo nutzen. |
Browser bleibt bei h2, curl --http3-only läuft ins Timeout | UDP 443 kommt nicht durch (Firewall, Docker-Mapping, Router, Cloud-Regel). | Erst lokal testen, dann Host-Firewall, dann Container-Ports, dann Router. |
Server ist erreichbar, aber kein alt-svc-Header | Header fehlt oder ist an eine Bedingung geknüpft. | always ergänzen und mit curl -sI prüfen. |
| h3 funktionierte, plötzlich nicht mehr | quic_host_key nicht gesetzt und Instanz/Socket neu gestartet, oder UDP-Blockade neu dazugekommen. | Schlüssel fest setzen, Firewall-Regel und Provider-Änderungen kontrollieren. |
| Hohe CPU-Last unter Last | QUIC-Verarbeitung pro Paket, fehlende Offloads. | quic_gso on, UDP-Puffer anheben, Paketraten beobachten. |
| Große Uploads brechen ab | Buffergrößen, MTU/Path-MTU, VPN-Tunnel mit kleiner MTU. | http3_stream_buffer_size erhöhen, MTU im Pfad prüfen, testweise über h2 hochladen. |
Im Log steht überall HTTP/2.0 | Alle Testclients sprechen kein QUIC (oder der $http3-Eintrag fehlt im Format). | Mit curl --http3-only und einem frischen Browserprofil gegenprüfen. |
--http3-only-Check in dein Monitoring, nicht nur in den Aktionstag.
Gefahrlos einführen
| Schritt | Was du tust | Rückweg |
|---|---|---|
| 1. Bestandsaufnahme | Build prüfen, UDP-Regel vorbereiten, nginx -t als Referenzlauf. | – |
| 2. Ein Testserver | h3 zuerst auf einer Subdomain oder einem Test-vHost aktivieren, nicht auf der Hauptseite. | Zeilen entfernen, reload. |
| 3. Prüfen | Die vier Befehle aus dem Prüf-Abschnitt, danach ein echter Browser ohne VPN. | – |
| 4. Ausrollen | Alt-Svc auf den echten Servern setzen, ma ruhig auf einen Tag. | Alt-Svc-Zeile löschen – nach Ablauf der Merkzeit sind alle zurück auf h2. |
| 5. Beobachten | Anteil h3 im Log, CPU, UDP-Paketraten. | Im Zweifel zuerst den Client-Anteil prüfen, bevor du am Server drehst. |
reload ohne die quic-Listener
schaltet h3 ab, ohne bestehende Verbindungen zu verlieren.
Häufige Fragen (FAQ)
| Frage | Antwort |
|---|---|
| Ist HTTP/3 schneller? | Nur in bestimmten Situationen: bei hoher Latenz, Paketverlusten und Netzwechseln. Im LAN oder über eine gute Glasfaserverbindung ist der Unterschied im Rauschen. |
| Brauche ich es überhaupt? | Nein. Es ist eine Optimierung mit überschaubarem Aufwand. Wer viel mobile Besucher hat, profitiert; wer nur im eigenen Netz arbeitet, kann sich das sparen. |
| Kann ich HTTP/1.1 abschalten? | Technisch ja, sinnvoll nein: Alte Clients, manche Werkzeuge und Monitoring-Checks sprechen nur HTTP/1.1. Der Betrieb kostet nichts. |
| Was ist mit einem CDN wie Cloudflare davor? | Dann terminiert das CDN HTTP/3 und spricht mit deinem Server in der Regel HTTP/2. Deine nginx-Konfiguration ist dafür irrelevant – prüfe die Einstellung beim Anbieter. |
| Kostet HTTP/3 mehr Traffic? | Nein, praktisch gleich. QUIC hat einen etwas größeren Header-Anteil pro Paket, das fällt aber nicht ins Gewicht. |
| Kann ich QUIC angreifen lassen – DoS durch UDP? | QUIC ist gegen Spoofing und Amplification besser geschützt als pures UDP, aber nicht immun. quic_retry on und Rate-Limits vor dem Server sind die Basis; nginx selbst hat für QUIC keine eigenen Limits. |
| Warum steht im Log „HTTP/3.0“? | nginx schreibt die Protokollversion der Anfrage in die Request-Zeile. Das ist korrekt und der einfachste Beleg, dass h3 im Einsatz ist. |
| Erkenne ich HTTP/3 in Uptime-Kuma? | Standard-Checks laufen über HTTP/1.1 oder HTTP/2. Ein HTTP/3-Monitor braucht einen Client mit QUIC – deshalb h3 wie oben beschrieben separat prüfen. |
| Muss ich etwas am Zertifikat ändern? | Nein. Gleiches Zertifikat, gleiche Erneuerung, nur der Reload muss weiterhin funktionieren. |
Fazit
HTTP/3 ist keine Revolution, sondern eine Ergänzung: ein zweiter Transportweg über UDP, der dort gewinnt, wo TCP nervt – Mobilfunk, Verluste, Netzwechsel. Technisch ist der Aufwand klein, weil die Zertifikate und die TLS-Einstellungen der ersten beiden Teile unverändert weiterverwendet werden. Gefährlich ist nur ein Punkt: zu glauben, es funktioniere, weil die Konfiguration grün ist.
Deshalb ist der wichtigste Teil dieses Artikels nicht der Serverblock, sondern die Prüfung – vier Befehle, die zeigen, ob QUIC wirklich verhandelt wird, und ein UDP-Port, der in den meisten Setups vergessen wird.
nginx -V zeigt, ob dein Build --with-http_v3_module hat.
③ Alt-Svc mit always ist der Werbeträger für h3 – ohne ihn probiert es kein Browser.
④ QUIC braucht TLS 1.3 und Session-Tickets für 0-RTT – beides hängt an den Entscheidungen aus Teil 2.
⑤ Ein h3-Ausfall ist lautlos: Prüfen mit curl --http3-only und ins Log schauen.