L4 · transport · reviewed

QUIC

QUIC

UDP-carried, always encrypted multiplexed transport: streams do not share TCP’s head-of-line block, and connection IDs survive path changes.

Presenter modeEmbed this figure

OverviewRFC 9000 · PROPOSED STANDARD · May 2021Long Header PacketsRFC 9000 · PROPOSED STANDARD · May 2021Connection MigrationRFC 9000 · PROPOSED STANDARD · May 2021StreamsRFC 9000 · PROPOSED STANDARD · May 2021IntroductionRFC 9114 · PROPOSED STANDARD · June 2022

Why it exists

TCP+TLS costs an extra round trip before application data and couples every multiplexed stream to one ordered byte stream. QUIC combines transport and cryptographic handshake and gives each stream independent delivery over UDP. OverviewRFC 9000 · PROPOSED STANDARD · May 2021

Initial long header starts the handshake

The client sends a QUIC Initial inside UDP — already carrying cryptographic handshake data. After 1-RTT the application can send. There is no TCP three-way handshake to watch for.

The client sends a long-header Initial packet (Header Form 1, type Initial) in a UDP datagram. Version is 1. Destination Connection ID is chosen so the server can route the handshake; TLS ClientHello is already inside the protected payload. Client: Header long · type Initial. UDP path. Server · :443.

ClientTLS inside first flightHeader: long · type InitialUDP pathServer · :443
  • Link
  • Blocking
  • Packet in flight
  • Discarded
  • Emphasis
Select a device to read its state. Arrow keys walk the topology.
Text equivalent of this diagram
Devices and links at this step
ElementKindState
ClienthostHeader: long · type Initial
UDP pathcloud
Server · :443host
ClientUDP pathlinkup
UDP pathServer · :443linkup
1 / 4

The client sends a long-header Initial packet (Header Form 1, type Initial) in a UDP datagram. Version is 1. Destination Connection ID is chosen so the server can route the handshake; TLS ClientHello is already inside the protected payload.

What changed

  • Emphasis on Client
  • Initial: Client → UDP path
  • Client: Header → long · type Initial
  • TLS inside first flight

How it works

Long headers (Initial, Handshake, 0-RTT, Retry) carry version and connection IDs during setup. Short headers carry 1-RTT data afterward. Long Header PacketsRFC 9000 · PROPOSED STANDARD · May 2021

Streams multiplex application data. Loss recovery is per-packet; a lost packet stalls only streams that needed those frames. StreamsRFC 9000 · PROPOSED STANDARD · May 2021

Connection IDs identify the connection across address changes. Migration probes a new path without tearing down the session. Connection MigrationRFC 9000 · PROPOSED STANDARD · May 2021

HTTP/3 maps HTTP semantics onto QUIC streams. QUIC itself is a general transport; HTTP/3 is one application. IntroductionRFC 9114 · PROPOSED STANDARD · June 2022

On the wire

Constructed examples, encoded from the field table below them — not captured traffic.

Long header Form=1, Fixed Bit=1, type Initial (00), version 1, eight-octet destination connection ID, empty source connection ID. Token Length and protected payload follow on the wire.

UDP
QUIC is always carried in UDP. Destination port 443 is common for HTTP/3; the four-tuple is not the connection identity — connection IDs are. RFC 9000
QUIC long header
Header Form 1, Fixed Bit 1, Long Packet Type, Type-Specific Bits, Version, Destination and Source Connection ID lengths and values, then type-specific fields. RFC 9000
Type-specific payload
For Initial: Token Length (varint), Token, Length (varint), Packet Number, and protected payload. Those fields are not fixed-width on the wire. RFC 9000

Configure it

Listen on UDP/443 for HTTP/3 and advertise h3 via Alt-Svc.

nginx (http_v3) 1.25+ · nginx with QUIC/HTTP/3draft

  1. server {
      listen 443 ssl;
      listen 443 quic reuseport;
      http2 on;
      http3 on;
      ssl_certificate     /etc/ssl/certs/example.pem;
      ssl_certificate_key /etc/ssl/private/example.key;
      add_header Alt-Svc 'h3=":443"; ma=86400' always;
    }

    QUIC requires a UDP listen alongside TCP TLS. Alt-Svc tells HTTP/1.1 and HTTP/2 clients that h3 is available on UDP/443.

    Common mistake: Opening only TCP/443 and wondering why no client ever sends an Initial — discovery never points at UDP.

    RFC 9114 §3.1

Verify

ss -ulnp | grep 443
UDP 443 bound by nginx.
curl --http3 -I https://example.com/
HTTP/3 response when the path permits UDP.

Caveats

  • Marked draft: nginx QUIC builds and directives change across releases.
  • Path firewalls must allow UDP/443 or clients fall back silently to HTTP/2.

When it breaks

Symptom first, because that is what you have when it happens.

  1. A client falls back to HTTP/2 over TCP and never uses HTTP/3, with no error shown.

    Narrow it down

    1. Check whether UDP 443 is reachable end to end.
    2. Look for an Alt-Svc header or an HTTPS DNS record advertising h3.
    3. Test from a network without a corporate middlebox.

    Cause

    Something is blocking or rate-limiting UDP. Clients treat QUIC as best-effort and fall back silently, which is good for users and makes the failure invisible to operators.

    Fix

    Permit UDP 443. The silence is by design, so this needs to be checked deliberately rather than waited for.

    Discovering an HTTP/3 EndpointRFC 9114 · PROPOSED STANDARD · June 2022
  2. QUIC throughput is well below TCP on the same path, on an otherwise healthy network.

    Narrow it down

    1. Check whether the receiver is doing UDP segmentation offload, or handling every datagram in software.
    2. Compare CPU usage during the transfer against a TCP transfer.
    3. Look at whether the path rate-limits UDP.

    Cause

    QUIC runs in userspace and its per-packet cost is paid by the CPU rather than the NIC. Without offload, a fast link can be limited by packet processing rather than by the network.

    Fix

    Enable generic segmentation offload for UDP where available, and measure CPU alongside throughput before concluding the network is at fault.

  3. Connections break when a client changes network, despite QUIC advertising connection migration.

    Narrow it down

    1. Check whether a middlebox on the path is keying state on the four-tuple.
    2. Confirm the server supports migration and has not disabled it.
    3. Look at whether the connection ID is being rewritten.

    Cause

    Migration works because a connection is identified by a connection ID rather than by addresses. Anything that rewrites or keys on the four-tuple — some load balancers — defeats it.

    Fix

    Route QUIC by connection ID at the load balancer. Hashing the four-tuple works right up until a client moves, which is the case migration exists for.

    Connection MigrationRFC 9000 · PROPOSED STANDARD · May 2021
  4. Handshake fails immediately; Version Negotiation or drops on first Initial.

    Narrow it down

    1. Confirm the client and server share QUICv1 (0x00000001).
    2. Check middleboxes that inspect or drop unknown UDP payloads.
    3. Capture whether the server’s Initial returns at all.

    Cause

    No shared version, or a middlebox treating long-header QUIC as suspicious UDP. Unlike TCP, there is no familiar handshake for legacy filters to whitelist by shape alone.

    Fix

    Allow UDP 443 end to end; ensure both ends negotiate version 1; avoid UDP payload inspection that assumes DNS-shaped datagrams.

    Version Negotiation PacketRFC 9000 · PROPOSED STANDARD · May 2021
  5. Intermittent failures behind a NAT or firewall that rewrites connection IDs or UDP ports aggressively.

    Narrow it down

    1. Compare connection IDs on the wire at client and server.
    2. Check whether the NAT times out UDP state shorter than the QUIC idle timeout.
    3. Look for symmetric NAT behaviour after migration probes.

    Cause

    QUIC assumes connection IDs and path validation behave as specified. Devices that mutate CIDs or drop UDP mappings early break sessions that TCP’s longer-lived state machinery used to paper over.

    Fix

    Disable CID mutation; raise UDP timeouts for 443; prefer endpoints that issue new CIDs deliberately rather than relying on NAT stability.

    Connection IDRFC 9000 · PROPOSED STANDARD · May 2021

Design notes

Clients treat QUIC as best-effort and fall back to TCP silently when UDP fails. That is good for users and bad for operators who never look at Alt-Svc or UDP reachability. Discovering an HTTP/3 EndpointRFC 9114 · PROPOSED STANDARD · June 2022

Userspace QUIC pays per-packet CPU cost that NIC TCP offloads used to hide. Measure CPU before blaming the path for low throughput.

Load balancers must understand connection IDs if migration matters. Four-tuple ECMP is enough only until the client moves.

Misconceptions

QUIC is faster because UDP is faster than TCP.
UDP is not faster; it is emptier. QUIC reimplements everything TCP does — reliability, ordering, congestion control — on top of it, and gains from combining the transport and TLS handshakes into one round trip and from removing head-of-line blocking between streams. OverviewRFC 9000 · PROPOSED STANDARD · May 2021
HTTP/3 is QUIC.
HTTP/3 is HTTP semantics mapped onto QUIC. QUIC is a general transport and carries other things; the two are separate specifications for that reason. IntroductionRFC 9114 · PROPOSED STANDARD · June 2022
QUIC removes head-of-line blocking entirely.
It removes it between streams: a lost packet stalls only the stream it belonged to, not every stream sharing the connection as in HTTP/2 over TCP. Within one stream, ordering still applies and a loss still blocks it.

More walkthroughs

Loss stalls one stream, not the connectioncomparison

HTTP/2 over TCP multiplexes streams on one byte stream — one lost segment stalls every stream. QUIC gives each stream its own delivery; loss isolates the damage.

The browser opens several streams on one QUIC connection: HTML, CSS, API. They share congestion control but not a single ordered byte pipe. Browser: Streams 0, 4, 8 open. Lossy hop. Origin.

BrowserStreams: 0, 4, 8 openLossy hopOrigin
  • Link
  • Blocking
  • Packet in flight
  • Discarded
  • Emphasis
Select a device to read its state. Arrow keys walk the topology.
Text equivalent of this diagram
Devices and links at this step
ElementKindState
BrowserhostStreams: 0, 4, 8 open
Lossy hopcloud
Originhost
BrowserLossy hoplinkup
Lossy hopOriginlinkup
1 / 3

The browser opens several streams on one QUIC connection: HTML, CSS, API. They share congestion control but not a single ordered byte pipe.

What changed

  • Browser: Streams → 0, 4, 8 open
  • multi-stream: Browser → Lossy hop

Connection ID survives a path changebaseline

A client moves from Wi-Fi to cellular. The connection continues because it is identified by connection ID. Load balancers that hash only the four-tuple break that promise.

The connection is established on Wi-Fi. Packets carry connection ID C1. The server and a CID-aware load balancer pin state to C1. Phone · CID C1: Path Wi-Fi four-tuple. Wi-Fi path. Cellular path. LB: Route by CID C1. Server.

Phone · CID C1Path: Wi-Fi four-tupleWi-Fi pathCellular pathLBRoute: by CID C1Server
  • Link
  • Blocking
  • Packet in flight
  • Discarded
  • Emphasis
Select a device to read its state. Arrow keys walk the topology.
Text equivalent of this diagram
Devices and links at this step
ElementKindState
Phone · CID C1hostPath: Wi-Fi four-tuple
Wi-Fi pathcloud
Cellular pathcloud
LBrouterRoute: by CID C1
Serverhost
Phone · CID C1Wi-Fi pathlinkup
Phone · CID C1Cellular pathlinkup
Wi-Fi pathLBlinkup
Cellular pathLBlinkup
LBServerlinkup
1 / 4

The connection is established on Wi-Fi. Packets carry connection ID C1. The server and a CID-aware load balancer pin state to C1.

What changed

  • CID C1: Phone · CID C1 → Wi-Fi path
  • LB: Route → by CID C1
  • Phone · CID C1: Path → Wi-Fi four-tuple

Terms

Connection ID
A QUIC identifier for a connection that is independent of the UDP four-tuple. It is what makes connection migration possible when addresses change.
QUIC stream
An independent ordered byte sequence within a QUIC connection. Loss stalls only streams that needed the lost frames — not every stream on the connection as with HTTP/2 over TCP.
HTTP/3
HTTP semantics mapped onto QUIC. QUIC is the transport (RFC 9000); HTTP/3 is the mapping (RFC 9114) — they are not the same specification.

Check yourself

  • A client silently uses HTTP/2 instead of HTTP/3. What should you check first?
  • What head-of-line blocking does QUIC remove?
  • How does a QUIC connection survive a client changing networks?
  • Why can QUIC use more CPU than TCP for the same throughput?
  • Is QUIC fast because UDP has less overhead than TCP?
  • How does HTTP/3 relate to QUIC?
  • What does Header Form = 1 mean in a QUIC packet?
  • How should a load balancer route QUIC if clients migrate?