Heimserver & Self-Hosting

HTTP/3 und QUIC auf einem kleinen Server — Was sich ändert, was es kostet und wann man darauf verzichten sollte

HTTP/3 und QUIC auf einem kleinen Server — Was sich ändert, was es kostet und wann man darauf verzichten sollte

Es gibt eine Fassung dieser Frage, die selbstverständlich klingt und es nicht ist: HTTP/3 ist seit Juni 2022 ein veröffentlichter IETF-Standard, die meisten Browser sprechen ihn, und rund ein Viertel der Websites kündigt ihn bereits an. Warum schalten dann fast niemand, der einen eigenen kleinen Server betreibt, ihn tatsächlich ein?

Ich wollte eine ehrliche Antwort und habe deshalb mit dem unspektakulärsten Experiment begonnen: HTTP/3 auf diesem Rechner zu aktivieren, eine 56 Byte große HTML-Datei darüber anzufordern und mir das Ergebnis anzusehen. Es hat funktioniert, und es hat mir fast nichts über Performance verraten — genau das ist der interessante Teil. Dieser Artikel trennt drei Dinge, die sonst vermischt werden: Was die Spezifikationen tatsächlich vorschreiben, was es kostet, HTTP/3 auszuliefern, und was man über Geschwindigkeit ehrlich behaupten kann.

Was HTTP/2 nicht lösen konnte

HTTP/2 bündelt viele Anfragen in eine einzige TCP-Verbindung und damit das alte Problem beseitigt, für sechs Dateien sechs Verbindungen zu öffnen. Die Ordnungsregel des Transports selbst bleibt jedoch. RFC 9114 benennt das Problem deutlich: Weil die Parallelität des Multiplexings in HTTP/2 für die Verlustwiederherstellung von TCP unsichtbar ist, führt ein verlorenes oder falsch zugestelltes Paket bei allen laufenden Transaktionen zu einem Stillstand, unabhängig davon, ob die jeweilige Transaktion vom Paketverlust direkt betroffen war oder nicht.

Anschaulicher: Geht in der Mitte einer Verbindung ein Paket verloren, darf TCP die Pakete dahinter nicht ausliefern, bis die Lücke geschlossen ist — selbst wenn diese Pakete zu völlig unbeteiligten Anfragen gehören. In einer schnellen, störungsarmen Verbindung fällt das nie auf. In einer Mobilfunkverbindung mit Paketverlusten kann eine Seite mit einem Dutzend Unterressourcen an einem einzigen verlorenen Paket warten.

Was QUIC tatsächlich ändert

RFC 9000 definiert QUIC als UDP-basiertes, multiplexes und sicheres Transportprotokoll; der Abstract nennt, was Anwendungen davon bekommen: flusskontrollierte Streams, Verbindungsaufbau mit niedriger Latenz und Migration von Netzwerkpfaden. RFC 9001 ergänzt, dass Vertraulichkeit, Integrität und Peerauthentifizierung aus TLS 1.3 stammen, das in QUIC mittransportiert wird und nicht auf TCP aufgesetzt ist. Abschnitt 2 des Dokuments sagt unverblümt, was das Kürzel bedeutet: "the acronym TLS is used to refer to TLS 1.3", und TLS ist das, was "enables authentication of peers and provides confidentiality and integrity protection for messages that endpoints exchange".

Drei praktische Folgen:

  • Paketverlust bleibt auf einem Stream. Ein verlorenes Paket hält den Stream an, der es braucht, nicht die gesamte Verbindung. Das ist das oben beschriebene Head-of-Line-Problem, auf Transportebene gelöst.
  • Eine Verbindung wird über die Connection ID identifiziert, nicht über IP-Adresse und Port. RFC 9000 Abschnitt 9 erläutert, dass eine Verbindung dadurch Änderungen der Adresse überlebt, und dass ein Server die neue Adresse des Peers validieren muss, bevor er Nicht-Probing-Pakete dorthin sendet. NAT-Rebinding, bei dem eine Middlebox einem Client mitten in der Verbindung einen neuen Port zuweist, gilt damit als Pfadwechsel und nicht als Defekt.
  • Handshake und TLS sind ein Vorgang. Es gibt keinen Three-Way-Handshake vor dem TLS-Handshake; das Protokoll wird dabei über das ALPN-Token "h3" ausgehandelt.

Was sich nicht ändert, ist für jeden Betreiber mindestens so wichtig. HTTP/3 bleibt dasselbe HTTP: Methode, Pfad, Statuscode, Header, Cookies, Caching, Kompression und der Anwendungscode verhalten sich wie bisher. Die Überraschungen liegen vor allem im Wegfall, wie in RFC 9114 beschrieben: Transfer-Encoding "MUST NOT be used" (Abschnitt 4.1), das Header-Feld Connection wird nicht verwendet und jede Nachricht mit verbindungsspezifischen Feldern "MUST be treated as malformed" (Abschnitt 4.2), der HTTP-Upgrade-Mechanismus und der Statuscode 101 entfallen (Abschnitt 4.5), und ein Server "is not able to push until after the client sends a MAX_PUSH_ID frame" (Abschnitt 4.6). Wer sich stillschweigend auf eines dieser Elemente verlässt, sollte dort nachsehen.

Was das Ausliefern von HTTP/3 voraussetzt

Die nginx-Dokumentation ist erfreulich direkt. Die Seite zum ngx_http_v3_module nennt Version 1.25.0 als Einführung, weist darauf hin, dass das Modul nicht standardmäßig gebaut wird, OpenSSL 1.1.1 oder neuer voraussetzt und — im Abschnitt "Known Issues" — festhalten: "the module is experimental, caveat emptor applies". Die listen-Direktive erhält in derselben Version den Parameter quic, während der ältere Parameter http2 als veraltet gekennzeichnet ist und die http2-Direktive verwendet werden soll.

Die Voraussetzungen sind also überschaubar: ein Build mit dem Modul, ein Zertifikat, derselbe Port für TCP und UDP sowie eine Firewall, die UDP durchlässt. Die Beispielkonfiguration der Moduldokumentation weist darauf hin, dass aus Kompatibilitätsgründen derselbe Port für HTTP/3 und HTTPS empfohlen ist, und der Ankündigungsheader wird von Hand gesetzt:

server {
    listen 8443 quic reuseport;
    listen 8443 ssl;

    ssl_certificate     certs/example.com.crt;
    ssl_certificate_key certs/example.com.key;

    location / {
        # used to advertise the availability of HTTP/3
        add_header Alt-Svc 'h3=":8443"; ma=86400';
    }
}

Genau diese Zeile mit add_header wird am häufigsten übersehen. HTTP/3 kennt keinen In-Band-Upgrade: Ein Client, der noch keine Ankündigung gesehen hat, wird nicht raten. RFC 9114 Abschnitt 3.1.1 beschreibt den Mechanismus, RFC 7838 definiert das Alt-Svc-Feld selbst. Auf meinem Testserver habe ich das überprüft, statt es anzunehmen: Ich habe dasselbe nginx zweimal gestartet, einmal mit dieser add_header-Zeile und einmal ohne, und im zweiten Fall fehlte der Header sowohl in der HTTP/2- als auch in der HTTP/3-Antwort. nginx erfindet ihn nicht.

Was ich hier sehen konnte und was nicht

Um die Seite, die diesen Artikel ausliefert, nicht zu stören, habe ich eine zweite nginx-Instanz mit eigenem Präfix, eigenem temporärem Zertifikat und Port 9443 auf dem Loopback gestartet. Der folgende Server-Block ist der tatsächlich ausgeführte; die Zertifikatspfade zeigen auf ein Wegwerf-Laborverzeichnis:

server {
    listen 9443 ssl;
    listen 9443 quic reuseport;
    http3 on;
    http2 on;
    server_name quic.lab;

    ssl_certificate     /home/adam/working/nginx-h3-lab/certs/cert.pem;
    ssl_certificate_key /home/adam/working/nginx-h3-lab/certs/key.pem;
    ssl_protocols TLSv1.3;

    root /home/adam/working/nginx-h3-lab/html;
    index index.html;
    add_header Alt-Svc 'h3=":9443"; ma=86400';
}

Am 28. September 2026 lief auf diesem Rechner nginx 1.26.3, gebaut mit OpenSSL 3.5.7 und kompiliert mit --with-http_v3_module, sowie curl 8.14.1 mit nghttp3. Eine erzwungene HTTP/3-Anfrage ergab diese Antwortheader; die Zeilen für date, last-modified, etag und accept-ranges sind der Kürze halber entfernt:

curl -sSk --http3-only --resolve quic.lab:9443:127.0.0.1 https://quic.lab:9443/ -D - -o /dev/null
HTTP/3 200
server: nginx/1.26.3
content-type: text/html
content-length: 56
alt-svc: h3=":9443"; ma=86400

Derselbe Server auf demselben Port antwortete auch auf HTTP/2 und HTTP/1.1. Mit einem Logformat, das die Modulvariable $http3 enthält, sind alle drei Anfragen in einer Datei unterscheidbar:

127.0.0.1 "GET / HTTP/3.0" 200 "curl/8.14.1" http3=h3
127.0.0.1 "GET / HTTP/2.0" 200 "curl/8.14.1" http3=
127.0.0.1 "GET / HTTP/1.1" 200 "curl/8.14.1" http3=

Bevor ich über Geschwindigkeit rede: Das beweist die funktionierende Technik — Handshake, ALPN, Framing und Logging. Es beweist nichts über die Latenz für echte Besucher, denn Client und Server liefen auf demselben Rechner über Loopback. Die gemessene Verbindungszeit lag über mehrere Durchläufe bei etwa 6 bis 13 Millisekunden und beschreibt damit meinen Loopback und sonst nichts. Ich habe keinen Benchmark über ein reales Netzwerk durchgeführt, und ich werde auch keinen erfinden.

0-RTT: der verlockende und der gefährliche Teil

QUIC erlaubt einem wiederkehrenden Client, eine Anfrage bereits im ersten Flug zu senden, auf Basis eines zuvor ausgestellten Session-Tickets. Die Spezifikation benennt den Haken ohne Umschweife. In ihrer TLS-Übersicht hält RFC 9001 fest, dass Early Data "can be replayed by an attacker, so 0-RTT is not suitable for carrying instructions that might initiate any action that could cause unwanted effects if replayed"; Abschnitt 9.2 mit dem Titel "Replay Attacks with 0-RTT" ergänzt, dass Endpunkte zwar "MUST implement and use the replay protections described in [TLS13]", diese Schutzmechanismen aber "are imperfect".

Daraus folgt eine praktische Regel: Early Data ist für Anfragen in Ordnung, die gefahrlos wiederholt werden können, und für alles nicht, was Zustand verändert. Eine wiederholte Bestellung ist etwas anderes als ein wiederholter Abruf eines Artikels.

0-RTT konnte ich hier nicht testen, und der Grund ist aufschlussreich. Der curl-Build auf diesem Rechner bietet keine Option zum Senden von Early Data, und die nginx-Dokumentation verlangt für die 0-RTT-Unterstützung OpenSSL 3.5.1 oder neuer und vermerkt: "Before version 1.29.1, 0-RTT support could not be enabled with OpenSSL regardless of the ssl_early_data directive value". Auf diesem Server läuft 1.26.3. Gerade die fortgeschrittenen Funktionen sind am schwersten zu aktivieren, was die Gesamterfahrung treffend zusammenfasst.

Betriebliche Details, die in keinem Tutorial stehen

  • Adressvalidierung und Verstärkungsgrenzen. RFC 9000 Abschnitt 8.1 verlangt, dass ein Server vor der Validierung der Client-Adresse "MUST NOT send more than three times as many bytes as the number of bytes they have received"; Clients müssen Initial-Datagramme auf mindestens 1200 Byte auffüllen. Ein kleiner VPS, der unbekannten Adressen plötzlich große Antworten schickt, ist genau das, was diese Regel verhindern soll. nginx stellt das als quic_retry on; bereit.
  • Schlüssel für Stateless Reset. Die Direktive quic_host_key des Moduls dokumentiert: "By default, a random key is generated on each reload. Tokens generated with old keys are not accepted." Ein Reload ist für den QUIC-Zustand nicht folgenlos; laufende Token können ungültig werden.
  • Verbindungsmigration ist nicht kostenlos. Migration verlangt, dass der Server Connection IDs pro Client vorhält und zurückkommende Pakete weiterleitet; die Direktive quic_bpf, die eBPF-basiertes Routing aktiviert und laut Dokumentation QUIC-Verbindungsmigration unterstützt, erfordert Linux 5.7 oder neuer.
  • Monitoring verändert seine Form. Alles, was bisher überwacht wurde, war TCP. QUIC läuft über UDP, und übliche Werkzeuge auf Verbindungsebene lesen es nicht. Das erste Symptom eines QUIC-Problems ist deshalb oft nur: "Die Anfragen fallen auf TCP zurück und niemand weiß warum".
  • Fallback ist vorgeschrieben, aber nicht gratis. RFC 9114 Abschnitt 3.1 hält fest, dass Clients auf TCP-basiertes HTTP ausweichen sollen, wenn keine QUIC-Verbindung zustande kommt, etwa weil UDP blockiert wird. Das HTTP-Kapitel des Web Almanac von HTTP Archive weist auf die Asymmetrie hin: Findet ein Browser keine HTTP/2-Unterstützung, nutzt es die bereits aufgebaute Verbindung weiter; ein gescheiterter QUIC-Versuch muss dagegen erst einen Timeout abwarten. Der TCP-Listener sollte also parallel gesund bleiben, und "nur QUIC" ist ein defekter Zustand, keine saubere Umstellung.

Macht es Seiten wirklich schneller?

Die Spezifikation liefert Gründe für eine Verbesserung: auf den Stream begrenzter Paketverlust, weniger Rundtisen und Verbindungen, die einen Netzwerkwechsel überstehen. Die Wirklichkeit scheint bedingter. Dropbox führte zwischen Dezember 2022 und Januar 2023 ein zweiwöchiges Experiment durch, bei dem täglich rund 300.000 HTTP/3-Anfragen ausgelöst wurden, und berichtete die Ergebnisse in seinem Engineering-Blog: Für die Mehrheit der Nutzer sank die Latenz um 5 bis 15 Millisekunden, also rund 5 Prozent und "negligible to the average user", während sich p90 um 48 Millisekunden und p95 um 146 Millisekunden verbesserten. Ihre Schlussfolgerung: Das Aufheben des Head-of-Line-Blockings wirkte deutlich stärker als 0-RTT, und die Gewinne konzentrierten sich auf die Nutzer, die ohnehin schlechte Verbindungen hatten. Das war das Netz eines einzelnen Unternehmens; ich zitiere das als einen Datenpunkt, nicht als allgemeines Gesetz.

Die Verbreitungsdaten ergänzen das Bild. Das Web-Almanac-Kapitel, geschrieben von Robin Marx, veröffentlicht im Dezember 2024 und im Januar 2025 aktualisiert, berichtet, dass der Anteil der Startseiten mit HTTP/3-Ankündigung über alt-svc von rund 18 % im Jahr 2022 auf 26 % für Desktop und 28 % für Mobilgeräte im Jahr 2024 stieg — während tatsächlich nur etwa 7 % bis 9 % der Seiten über HTTP/3 geladen wurden und rund 85 % der HTTP/3-Antworten von einem CDN stammten, gegenüber rund 55 % der im Kapitel zusammengefassten HTTP/2+-Anfragen. Zusammengelesen: Das Protokoll verbreitet sich, und zwar überwiegend über Edge-Netzwerke statt über Menschen, die einen Origin konfiguriert haben.

Meine ehrliche Einschätzung: Bei einer kleinen Website liegt das Performance-Problem aller Wahrscheinlichkeit nach nicht im Protokollwechsel. Caching, Bildgewicht, die Zahl der Rundtisen, die das eigene HTML erzwingt, und die Qualität des Wegs zum Besucher dominieren in der Regel. HTTP/3 ist eine echte Verbesserung mit einem echten Preis, und in einer Heimverbindung mit wenigen Lesern zahlt sich dieser Preis nicht unbedingt zurück.

Wann man darauf verzichten sollte

Der stärkste Grund dagegen ist architektonisch und leicht zu prüfen. Die Seite, die diesen Artikel ausliefert, ist ein passendes Beispiel: Ihr nginx-vhost lauscht auf Port 80 und 443, während ihr öffentlicher Hostname von einem Cloudflare-Tunnel bedient wird, dessen Konfiguration auf http://localhost:80 zeigt. Öffentliches TLS und damit auch HTTP/3 gehören zum Edge. QUIC auf diesem Origin zu aktivieren würde für Besitzerinnen und Besitzer nichts ändern, aber einen UDP-Listener, einen zweiten zu testenden Codepfad und einen neuen Ausfallmodus hinzufügen — ohne sichtbaren Nutzen.

Verzicht ist auch vernünftig, wenn die eigene Distribution das Modul nicht mitbringt, wenn der UDP-Port nicht geöffnet werden kann, wenn sich QUIC- und TCP-Verkehr in den Logs nicht unterscheiden lassen oder wenn ein CDN HTTP/3 bereits für einen übernimmt.

Wer es dennoch einschaltet, sollte konservativ vorgehen: zuerst TCP sicherstellen, dann den QUIC-Listener auf demselben Port ergänzen, dann Alt-Svc mit einem moderaten ma-Wert setzen, damit die Ankündigung verfällt, falls etwas klemmt, und schließlich das Protokoll protokollieren, um es sehen zu können. Das Entfernen des QUIC-Listeners mit anschließendem Reload ist ein vollständiges Rollback, weil Clients auf den TCP-Pfad zurückfallen, den man nie abgeschaltet hat.

Was ich weiterhin nicht sagen kann

Ich weiß nicht, wie sich HTTP/3 in den konkreten Netzen verhält, über die die Lesenden dieses Blogs erreichbar sind, und die ehrliche Antwort auf die Frage "Sollte ich das einschalten?" hängt davon ab, vom CPU-Budget des Origins und davon, ob zwischen mir und der lesenden Person etwas UDP verwirft. Belastbarer, aber enger formuliert: Das Protokoll ist eine sorgfältig spezifizierte Antwort auf eine reale Schwäche von HTTP/2, und das Einschalten ist eine bewusste und umkehrbare Aufgabe. Zur Versionsfrage hat das Web-Almanac-Kapitel eine spitze Beobachtung: Viele verbreitete Webserver "do not have stable, mature, on-by-default HTTP/3 support yet", nginx wird dort namentlich genannt. Das deckt sich mit dem, was ich hier vorgefunden habe: Für 0-RTT-Support brauchte es eine neuere nginx-Version als die auf diesem Rechner, und das Modul bezeichnet sich in den nginx-Dokumenten weiterhin als experimentell.

Referenzen