Skip to content

VOL 04 / CH 11 / LESSON 01

11.1 QUIC: Connections, Streams, Loss Recovery and Migration ​

HTTP/2 puts multiple requests on one TCP connection, but a lost TCP segment can still make unrelated requests wait. QUIC provides encryption and multiple reliable streams over UDP, keeping the wait for ordered delivery within each stream. This lesson uses packet numbers, stream offsets, and acknowledgments to explain which delays disappear and what still needs handling during loss, congestion, or a network change.

QUIC v1's core specifications, published in 2021, are RFC 9000, 9001, and 9002. They cover transport, TLS integration, and loss detection with congestion control. QUIC provides reliable streams, flow control, and connection migration without copying all of TCP's semantics.

1. How multiple streams reduce head-of-line blocking ​

HTTP/2 multiplexes HTTP streams over one TCP byte stream. When a TCP segment is lost, the TCP receiver must fill the byte gap before delivering subsequent bytes in order. Data belonging to another request can be trapped behind that gap too. This is transport-level head-of-line blocking (HOL).

QUIC reassembles bytes separately for each stream. Suppose stream A is missing offsets [0, 100), while stream B has received a complete response starting at offset 0. B can proceed without waiting for A's gap. Several dependencies remain:

  • Ordered delivery within one stream still requires filling its gaps.
  • Streams share congestion control, so loss and queueing can reduce their available sending rate.
  • Exhausted connection-level flow-control credit can block multiple streams.
  • HTTP/3 QPACK dynamic-table dependencies can block field decoding.
  • If the application needs A's result before processing B, the transport cannot remove that dependency.

TCP still has mature hardware offload, network compatibility, and broad API support. QUIC integrates transport, encryption, and multiplexing, while common userspace implementations can evolve with applications or libraries. A protocol upgrade cannot cancel a dependency the application actually needs.

2. UDP provides datagrams; QUIC supplies connection semantics ​

The diagram shows the layers. DATAGRAM is an optional extension:

text
HTTP/3 or other application protocol
              |
QUIC streams / optional DATAGRAM extension / connection control
              |
UDP datagrams
              |
IP

UDP does not guarantee delivery, ordering, or deduplication. QUIC manages acknowledgments (ACKs), loss recovery, stream offsets, flow control, and congestion control. The QUIC DATAGRAM extension also supports unreliable application datagrams: they are congestion controlled, but QUIC does not retransmit them after loss, and they do not consume stream byte credit. RFC 9221 §5

Userspace implementations can evolve without waiting for kernel TCP updates, while still using UDP GSO/GRO, checksum offload, RSS, and batched I/O. Performance depends on the operating system, NIC, and library. Userspace alone implies neither higher speed nor the absence of hardware acceleration.

3. Packet numbers identify transmissions; offsets identify stream data ​

A QUIC packet can carry several frames, such as STREAM, ACK, CRYPTO, MAX_DATA, and PATH_CHALLENGE. Each sender's packet numbers increase within their packet-number space and are never reused. The wire encoding carries a truncated value of 1–4 bytes; the receiver reconstructs the full value using its expected number. Retry and Version Negotiation packets do not use these packet numbers.

QUIC v1 has three packet-number spaces: Initial, Handshake, and Application Data. 0-RTT and 1-RTT share Application Data, but 0-RTT packets cannot carry ACKs.

After loss, bytes requiring reliable delivery go into packets with new packet numbers. Frames can be split or combined again. This constructed sending trace uses half-open byte ranges:

Packet numberInformation carriedObserved state
5Stream 0 bytes [0, 100)Unacknowledged, later declared lost
6Stream 4 bytes [0, 80)Acknowledged, independently deliverable
17Stream 0 bytes [0, 100) sent againA later ACK covers packet number 17

An ACK acknowledges packet-number ranges. The sender's own records associate packet 17 with its bytes and update their delivery state. The ACK does not mean “17 retransmits 5.” If packet 5 was merely delayed, the receiver still avoids delivering duplicate stream bytes to the application. A transport ACK does not prove that the application processed a request; business results need application-level confirmation.

4. Loss thresholds and PTO have different jobs ​

An ACK frame can report several packet-number ranges. A gap means packets have not yet been acknowledged; loss or reordering could explain it. A gap alone is insufficient to declare loss immediately.

RFC 9002's two acknowledgment-based methods first require a newer packet in the same packet-number space to have been acknowledged.

  • Packet threshold: declare an unacknowledged packet lost when its distance from the largest acknowledged packet number reaches the threshold, initially recommended as 3. If the largest acknowledged packet number is 10, unacknowledged packet 7 has reached that threshold.
  • Time threshold: declare an earlier packet lost when enough time has elapsed since it was sent. The recommended threshold is max(9/8 × max(smoothed_rtt, latest_rtt), timer_granularity).

For a smoothed RTT of 40 ms, latest RTT of 48 ms, and timer granularity of 1 ms, the time threshold is 54 ms. A packet waiting for 60 ms still cannot be declared lost by this rule alone if no newer packet has been acknowledged. RFC 9002 §6.1

Probe Timeout (PTO) handles missing feedback: expiry sends an acknowledgment-eliciting probe, with exponential backoff for consecutive expirations. PTO expiry itself neither declares earlier packets lost nor directly shrinks the congestion window. Subsequent loss or persistent-congestion detection triggers the corresponding congestion response. RFC 9002 §6.2

Acknowledgment delay has limits too. Receivers should immediately acknowledge ack-eliciting Initial and Handshake packets instead of applying application-data delayed-ACK policies. Senders ignore the reported ACK Delay when calculating RTT for those two spaces.

5. How QUIC uses TLS 1.3 ​

QUIC v1 uses the TLS 1.3 handshake without carrying the TLS record layer. QUIC CRYPTO frames carry handshake messages. TLS supplies key derivation and peer authentication; QUIC protects packets.

Encryption levelPurposeBoundary
InitialCarries ClientHello and other initial handshake messagesKeys derive from the client's Initial destination CID and a version-specific public salt; observers can calculate them too
HandshakeProtects later handshake messagesUses keys derived from TLS handshake secrets
0-RTTSends early application data during resumptionCan be replayed, lacks forward secrecy, and may be rejected
1-RTTProtects normal application data and control informationUses application traffic keys and supports key updates

Initial packets use encryption, but provide neither confidentiality against observers nor reliable peer authentication. Their protection discourages middleboxes from depending on cleartext handshake formats; it does not conceal a ClientHello that has no separate protection. RFC 9001 §5.2

Header protection mainly masks packet numbers and selected flag bits. Routing information such as CIDs remains visible, as does the version in a long header. Authenticated encryption detects tampering when the protection keys are secret. Initial's publicly derivable keys do not provide the same authentication guarantee.

6. First connections, resumption, and 0-RTT ​

Without Retry, loss, or other extra round trips, a new QUIC connection usually lets the client send application data protected with 1-RTT keys after 1 RTT. Serial TCP and TLS 1.3 setup usually takes 1 RTT for TCP and another for TLS. This compares connection establishment, not total page-load time.

Browsers also reuse connections, preconnect, resume TLS sessions, resolve DNS, and may race other protocols. A fixed RTT saving on every visit is therefore not a useful promise.

0-RTT depends on resumable session state or a ticket. The client sends early data before the handshake completes, and the server may reject it. After rejection, the client and application must follow their interface contract to resend in the 1-RTT phase without losing data or processing it twice.

An attacker may replay early-data ciphertext even though it is encrypted. An HTTP service can use 425 Too Early to request a retry after the handshake; that retry must not use early data again. Idempotency does not make all effects harmless: repeated GET or PUT requests can still increase billing, auditing, cache-warming, or notification costs. Review side effects, credential scope, and distributed replay prevention for each endpoint before enabling 0-RTT. RFC 8470 §5.2

7. Stream IDs and flow control ​

Streams vary along two axes: client or server initiated, and bidirectional or unidirectional. The least significant stream-ID bit identifies the initiator; the next bit identifies direction. IDs within one type therefore increase by 4.

This Python 3 program demonstrates only the numbering rule and opens no network connection. Save it as stream_ids.py and run python3 stream_ids.py:

python
for first_id in range(4):
    initiator = "server" if first_id & 1 else "client"
    direction = "uni" if first_id & 2 else "bidi"
    ids = [first_id + 4 * n for n in range(3)]
    print(f"{initiator} {direction}: {ids}")

Expected output:

text
client bidi: [0, 4, 8]
server bidi: [1, 5, 9]
client uni: [2, 6, 10]
server uni: [3, 7, 11]

Byte offsets determine order within a stream. A packet can contain frames from several streams, and retransmitted stream data can be framed differently. An individual encoded frame does not span two QUIC packets.

Flow control has two levels: MAX_STREAM_DATA limits the byte extent of one stream, while MAX_DATA limits aggregate usage across streams. Retransmitting existing bytes consumes no additional credit. Sparse offsets and a stream's final size also count, so accounting cannot just sum bytes the application has received.

MAX_STREAMS limits the cumulative number of streams the peer may open, separately for bidirectional and unidirectional streams. As the receiver consumes data and reclaims stream resources, it needs to increase the appropriate limits. Applications still need their own limits on concurrent requests and buffered memory.

8. CIDs identify connections; path validation checks addresses ​

A Destination Connection ID (DCID) helps receivers and load balancers find existing connection state after a source address or port changes. QUIC v1 CIDs can be up to 20 bytes long. Zero length is allowed, but the destination CID a client chooses before receiving a server Initial or Retry must be at least 8 bytes long. Zero-length CIDs require other information, such as addresses, for demultiplexing and generally make connection continuity through address changes difficult.

Peers supply spare CIDs with NEW_CONNECTION_ID and signal retirement with RETIRE_CONNECTION_ID. Changing CIDs reduces cross-path linkability, but packet sizes, timing, and IP addresses can still reveal associations. CID rotation does not provide anonymity.

In QUIC v1, clients initiate active migration after handshake confirmation. PATH_CHALLENGE / PATH_RESPONSE validates whether the peer receives packets at the new address. Path transmission and anti-amplification rules apply before validation. The disable_active_migration parameter can prohibit active migration, but cannot prevent a NAT from changing an address or port.

A new path may have different capacity and RTT. After confirming a new address, endpoints normally reset congestion and RTT state. If only the peer's port changes, the RFC permits retaining that state because NAT rebinding commonly causes such changes. RFC 9000 §9.4

Connection continuity also requires packets to reach the endpoint holding the keys and stream state. A CID does not copy that state to another server. The next lesson covers load balancing.

9. Address validation, the three-times limit, and datagram size ​

An attacker can spoof a UDP source address. Before validating a client's address, a server must send no more than three times the bytes it has received from that address. If qualifying received datagrams total 1200 bytes, the current sending budget is at most 3600 bytes. This limits bytes, not packet count.

A server can use Retry and a token to require proof that the client receives replies before allocating more state or sending more data. Retry adds a round trip, creating a trade-off between denial-of-service protection, state cost, and handshake latency.

Every client UDP datagram containing an Initial packet needs a UDP payload of at least 1200 bytes, using padding or coalesced packets when necessary. That size excludes UDP/IP headers and does not require every post-handshake packet to be equally large.

Endpoints should use Datagram Packetization Layer Path MTU Discovery (DPLPMTUD) or PMTUD to discover usable sizes. Without these mechanisms, they should conservatively limit datagrams to 1200 bytes. A path that cannot carry a 1200-byte UDP payload cannot continue carrying normal QUIC traffic. RFC 9000 §14

10. Check your understanding ​

  1. When stream A has a gap but stream B is complete, which waits disappear and which dependencies remain?
  2. Packets 5 and 17 in the table carry identical bytes. Why must their packet numbers still differ?
  3. Why is elapsed time alone insufficient for time-threshold loss detection without an ACK for a newer packet?
  4. Why do Initial keys fail to provide confidentiality against observers?
  5. If the example increments IDs by 1 instead of 4, which other stream types appear?
  6. What do CIDs, path validation, and congestion-state updates each do during migration?
  7. A server receives 1200 bytes and sends 4000 bytes to an unvalidated address. Which rule does it violate?

Next lesson: 11.2 HTTP/3, QPACK, Discovery and Deployment.

References ​

Built with VitePress | Software Systems Atlas