Protocol Deep Dives

QUIC Protocol: The Transport Layer Behind HTTP/3

Understanding QUIC — connection establishment, loss detection, congestion control, connection migration, and how it differs from TCP+TLS.

QUIC Design Goals — Why Reinvent TCP?

TCP has been the internet's reliable transport layer since 1974. It is baked into every operating system kernel, every router, and every middlebox on the network. Changing TCP itself is nearly impossible — any modification must be backward compatible with 50 years of deployed equipment.

QUIC (RFC 9000, 2021) started as a Google experiment in 2012 and became an IETF standard in 2021. Its design goals were explicit:

  • Eliminate TCP head-of-line blocking — loss on one stream must not stall others
  • Reduce connection latency — 1-RTT for new connections, 0-RTT for resumption
  • Encrypt everything — no cleartext transport, prevent ossification
  • Connection migration — survive IP address changes (mobile networks)
  • Userspace implementation — deployable without kernel changes

UDP Substrate

QUIC runs on UDP — not because UDP is better than TCP, but because UDP packets pass through NAT and firewalls without requiring special middlebox support, and because UDP datagrams can be processed in userspace. QUIC reimplements everything TCP provides (reliability, ordering, flow control, congestion control) but does so inside the encrypted payload where network devices cannot interfere.

Network Stack Comparison:

Traditional HTTPS:       QUIC + HTTP/3:
┌─────────────┐          ┌─────────────┐
│   HTTP/2    │          │   HTTP/3    │
├─────────────┤          ├─────────────┤
│   TLS 1.3   │          │    QUIC     │  ← reliability + crypto combined
├─────────────┤          ├─────────────┤
│    TCP      │          │    UDP      │
├─────────────┤          ├─────────────┤
│     IP      │          │     IP      │
└─────────────┘          └─────────────┘

Ossification Prevention

A key QUIC design insight: middleboxes can only ossify what they can see. Because QUIC encrypts almost everything (including most packet headers), network devices cannot inspect or modify QUIC internals. This means QUIC can evolve its transport semantics without being blocked by deployed hardware that expects specific byte patterns.

Connection Establishment

1-RTT Handshake

A QUIC connection combines transport and TLS negotiation into a single handshake:

Client                                    Server
  │                                          │
  │──── Initial (ClientHello) ───────────────▶│  RTT 0
  │     - QUIC version, connection ID         │
  │     - TLS ClientHello with key share      │
  │                                          │
  │◀─── Initial (ServerHello) ───────────────│
  │◀─── Handshake (EncryptedExtensions)  ────│  RTT 1 complete
  │◀─── Handshake (Certificate)          ────│
  │◀─── Handshake (CertificateVerify)    ────│
  │◀─── Handshake (Finished)             ────│
  │                                          │
  │──── Handshake (Finished) ────────────────▶│
  │──── 1-RTT (HTTP/3 HEADERS frame) ────────▶│  Application data
  │◀─── 1-RTT (HTTP/3 HEADERS + DATA) ───────│

After 1 round-trip, both sides have derived encryption keys and the client can send application data. Contrast with TCP + TLS 1.3 which requires the TCP handshake first (1 RTT) then the TLS handshake (1 more RTT) = 2 RTTs.

0-RTT Resumption

On a subsequent connection to a known server, QUIC can use a Pre-Shared Key stored from the previous session to encrypt the ClientHello AND application data together in the very first packet:

Client                                    Server
  │                                          │
  │──── Initial (0-RTT data + ClientHello) ──▶│  Application data sent immediately
  │◀─── Handshake + 1-RTT data ──────────────│  Response before RTT completes

The catch: 0-RTT data is not forward-secret (it uses the PSK, not a fresh key exchange) and is vulnerable to replay attacks. RFC 9001 restricts 0-RTT to applications that can tolerate replay — idempotent requests only.

Version Negotiation

QUIC includes version negotiation built into the Initial packet. If the server does not support the client's requested version, it sends a Version Negotiation packet listing supported versions. The current QUIC version is 1 (0x00000001). Version 2 (RFC 9369) shuffles packet type assignments to prevent middlebox ossification of version-1-specific patterns.

Stream Multiplexing — No Head-of-Line Blocking

Stream Types and IDs

QUIC streams are lightweight, independently-delivered byte-streams. Each stream has an ID encoded as a 62-bit integer with two low bits encoding type:

Stream ID encoding:
  Bits 63-2: stream number
  Bit 1: 0=client-initiated, 1=server-initiated
  Bit 0: 0=bidirectional, 1=unidirectional

Examples:
  0  (0b00)  Client-initiated bidirectional stream 0
  1  (0b01)  Client-initiated unidirectional stream 0
  2  (0b10)  Server-initiated bidirectional stream 0
  3  (0b11)  Server-initiated unidirectional stream 0
  4  (0b100) Client-initiated bidirectional stream 1

Independent Stream Delivery

Within a stream, data is delivered in order. Across streams, there is no ordering guarantee. The QUIC stack delivers stream data to the application as soon as it arrives in-order *for that stream*, regardless of what is happening on other streams.

Packet loss scenario:

Packets: [Stream4 chunk1] [Stream8 chunk1] [Stream4 chunk2] ← LOST ← [Stream8 chunk2]

TCP behavior:  Stream 8 chunk 2 blocks behind lost Stream 4 chunk 2
QUIC behavior: Stream 8 chunk 2 delivered immediately; only Stream 4 waits

Flow Control

QUIC implements two levels of flow control:

  • Stream-level: Each stream has a receive window. The sender cannot exceed the receiver's buffer capacity for that stream.
  • Connection-level: The entire connection has an aggregate receive window preventing any one peer from overwhelming the other.

Flow control windows are advertised via MAX_STREAM_DATA and MAX_DATA frames and updated by the receiver as it processes data.

Loss Detection and Recovery

Packet Numbering

Unlike TCP sequence numbers (byte offsets), QUIC uses packet numbers — each packet has a unique, monotonically increasing number. Retransmitted data is sent in a *new* packet with a *new* packet number. This eliminates TCP's retransmission ambiguity (was the ACK for the original or the retransmit?).

TCP retransmission ambiguity:
  Send: seq=1000  ← lost
  Retransmit: seq=1000  ← same number
  ACK for seq=1000 — which transmission caused it?

QUIC:
  Send: pn=42  ← lost
  Retransmit same data in: pn=47  ← new number
  ACK for pn=47 — unambiguous

ACK Frames

QUIC ACK frames acknowledge received packets using ranges, similar to TCP SACK:

ACK frame:
  Largest Acknowledged: 150
  ACK Delay: 2ms
  ACK Ranges: [130-150, 100-125]   ← gaps: 126-129 not received

QUIC supports up to 255 ACK ranges per frame, allowing very precise acknowledgment of sparse packet reception — important on lossy networks.

Congestion Control

RFC 9002 specifies QUIC's loss detection and congestion control algorithms, which are similar to modern TCP but implemented in userspace:

  • Loss detection: Uses ACK-based loss detection with a packet threshold (3 packets) or a time threshold (9/8 × max RTT)
  • Congestion control: QUIC implementations typically use CUBIC (same as Linux TCP) or BBR (Bottleneck Bandwidth and RTT)
  • Pacing: QUIC can pace packet transmission in userspace, spreading bursts over time to reduce queue buildup

Because congestion control runs in userspace, QUIC implementations can update their algorithms without OS changes — Google deployed BBRv2 in their QUIC implementation years before it was available in Linux TCP.

Connection Migration

Connection IDs Enable Migration

TCP connections are bound to a 4-tuple (src-IP, src-port, dst-IP, dst-port). Change any element and the connection breaks. QUIC connections are identified by Connection IDs — opaque byte strings chosen independently by each endpoint:

QUIC Long Header Packet (Initial/Handshake):
┌────────────────────────────────────────────────┐
│ Header Form=1 │ Fixed Bit=1 │ Packet Type (2b) │
│ Version (32 bits)                              │
│ DCIL (4b) │ SCIL (4b)                         │
│ Destination Connection ID (0-20 bytes)         │
│ Source Connection ID (0-20 bytes)              │
│ [type-specific fields]                         │
│ Payload (encrypted)                            │
└────────────────────────────────────────────────┘

Seamless Wi-Fi to Cellular Handoff

When a mobile device switches networks:

  • The client gets a new IP address (e.g., from Wi-Fi's 192.168.1.x to cellular's 10.x.x.x)
  • The client sends a PATH_CHALLENGE frame on the new path
  • The server responds with PATH_RESPONSE, verifying the new path works
  • The client confirms migration with a NEW_CONNECTION_ID frame
  • Traffic continues on the new path — the HTTP/3 request in progress resumes without interruption
Time:   [Wi-Fi]──────────────────────────[Cellular]
TCP:              connection broken ──────▶[new TCP handshake, new TLS]
QUIC:                      ─────── seamless migration ──────────────▶

For a video call or file download, QUIC migration means the transfer continues without a visible interruption — no re-buffering, no dropped call.

Active Connection ID Management

To prevent linkability attacks (where an observer correlates connection IDs across path changes to track a user), QUIC recommends using a *new* Connection ID after each migration. Endpoints pre-provision multiple Connection IDs with NEW_CONNECTION_ID frames so the client can switch IDs atomically with path migration.

Related Protocols

Related Glossary Terms

More in Protocol Deep Dives