§1.4
Chapter 01 · Foundations of QUIC and HTTP3

§1.4Packetization, Connection Identifiers, and Multiplexing Basics with Worked Examples

RFC 9000RFC 8999

Learning objective. Trace how stream bytes become frames, packets, and UDP datagrams; read the long and short QUIC headers field by field; and explain what Connection IDs do and why several QUIC packets can share one datagram.

From stream bytes to the wire #

QUIC nests four things. Application data lives in streams; stream bytes (and control signals) are carried in frames; frames are packed into the payload of a packet; and one or more packets are placed in a UDP datagram that is sent on the network ([RFC 9000 §12]). Multiplexing happens at the frame level: a single packet may carry a STREAM frame for stream 0, another STREAM frame for stream 4, and an ACK frame, all at once. There is no per-packet notion of "this packet belongs to one stream" — the frames inside decide.

Which frames a packet may carry is not open-ended, though: it depends on the packet's encryption level. Handshake-level packets (Initial, Handshake) carry only the frames needed to run the handshake and acknowledge it; application-level packets (0-RTT, 1-RTT) carry data and connection-control frames, with a handful (including ACK and CRYPTO) reserved for 1-RTT.

UDP datagram — one or more packets, possibly coalesced

Initial
long header

Handshake
long header

0-RTT
long header

1-RTT
short header

frames it may carry
PADDING · PING · ACK · CRYPTO
CONNECTION_CLOSE (0x1c)

frames it may carry
PADDING · PING · STREAM · RESET_STREAM · STOP_SENDING
MAX_DATA / MAX_STREAM_DATA / MAX_STREAMS · …_BLOCKED
NEW_CONNECTION_ID · RETIRE_CONNECTION_ID · PATH_CHALLENGE
DATAGRAM · CONNECTION_CLOSE

frames it may carry
everything 0-RTT carries, plus
ACK · CRYPTO · NEW_TOKEN · PATH_RESPONSE · HANDSHAKE_DONE

Fig. 1.4-3The containership of the wire — a datagram holds one or more packets, each packet holds frames — and which frames a packet may carry depends on its encryption level. Handshake packets (amber) run the handshake; application packets (cyan) carry data.RFC 9000 §12.4 (Table 3)

Among the application-level frames, the STREAM frame is the workhorse. It carries the reliable, ordered application data that is almost all of any connection's traffic — every HTTP/3 request and response travels in STREAM frames (§1.3). The DATAGRAM frame is a different tool: an optional extension ([RFC 9221]) for applications that want QUIC's encryption, congestion control, and multiplexing but not its reliability or ordering — real-time media, say (Chapter 9). Unless an application explicitly opts into datagrams, its data rides STREAM frames; treat STREAM as the default and DATAGRAM as the specialized exception.

Every packet begins with a header, and the first bit of the first byte selects which kind. Long headers (Header Form = 1) are used while the connection is being established: the Initial, 0-RTT, Handshake, and Retry packet types. Short headers (Header Form = 0) carry 1-RTT application data once the handshake is done ([RFC 9000 §17]). Two invariants hold across all QUIC versions: the header form bit and the presence of Connection IDs are fixed by RFC 8999, so even a version-ignorant observer can find the CID.

Why two headers at all? Because a connection's two phases have opposite needs. During establishment the endpoints share no context yet: they must agree on a QUIC version, each learn the other's Connection IDs, and label which encryption level a packet belongs to. The long header is therefore deliberately self-describing (it spells out the Version field, both Connection IDs and their lengths, and an explicit packet type) because nothing can be assumed and a fresh peer (or a load balancer) has to parse it cold. That same verbosity is what makes version negotiation and the RFC 8999 invariants possible in the first place.

Once the handshake finishes, almost all of that is fixed, and repeating it on every packet would be pure overhead, and steady-state data packets outnumber handshake packets by orders of magnitude. So the short header keeps only what the common case needs and drops the rest: no Version (it is constant for the connection's life), no Source Connection ID (the peer already knows the sender's CID, and only the Destination CID is needed to route a packet to its recipient), and not even a length for the Destination CID (the receiver chose its own CIDs, so it already knows how long they are). What is left is just the header byte, the Destination Connection ID, and the packet number, plus two steady-state-only bits: the Key Phase for key updates (§2.2) and the latency spin bit ([RFC 9000 §17.3]). The split is the same "bootstrap once, then run lean" instinct behind 0-RTT and coalescing: pay the metadata cost on the handful of handshake packets, and make the millions of data packets that follow as small as they can be.

The long header, field by field #

0156byte offsetHeaderByte0xC0-0xFF1|1|TT|xxxx8 b · 1 BVersion0x00000001 = v132 b · 4 BDCID Len0-208 b · 1 BDestinationConnection ID0..160SCID Len0-208 b · 1 BSource Connection ID0..160Type-Specific Payload0..*
Fixed-width fieldLength / count prefixVariable-length valueAEAD-protected / encrypted
Fig. 1.4-1QUIC long header (generic) — RFC 9000 §17.2RFC 9000 §17.2

The header byte packs the Header Form (1), Fixed Bit (1), Long Packet Type (2), and four type-specific bits, giving the value range 0xC00xFF (constants.md §3). The 32-bit Version field names the QUIC version (0x00000001 for v1). Then come two length-prefixed Connection IDs: the Destination CID (whom this packet is for) and the Source CID (whom to answer). Each length is an 8-bit field whose value must be 0–20 in v1; anything larger is invalid ([RFC 9000 §17.2]). The short header is leaner. Having established context, it drops the version and the Source CID entirely:

01byte offsetHeaderByte0x40-0x7F0|1|S|RR|K|PP8 b · 1 BDestinationConnection ID0..160Packet Number8..32Protected Payload(frames)0..*
Fixed-width fieldVariable-length valueAEAD-protected / encrypted
Fig. 1.4-2QUIC short header (1-RTT) — RFC 9000 §17.3RFC 9000 §17.3

The short header's flag bits carry the spin bit (S), two reserved bits (RR), the Key Phase (K, see §2.2), and the Packet Number Length (PP). Both the reserved bits and the packet number itself are hidden by header protection, so on the wire they look like noise ([RFC 9001 §5.4]); the packet number is recovered only after removing that protection.

Connection IDs: naming a connection #

A Connection ID lets a connection outlive its addresses. Because the receiver matches a packet to a connection by its Destination CID rather than by the UDP four-tuple, the client's IP and port can change (a NAT rebinding, a Wi-Fi-to-cellular handoff) and the connection continues once the new path is validated ([RFC 9000 §5.1], and Chapter 10). Endpoints issue multiple CIDs over the connection's life (via NEW_CONNECTION_ID frames) so a migrating client can switch to an unused one, preventing a passive observer from linking the old and new paths by a constant CID.

Two questions about the Destination Connection ID cause endless confusion, so pin them down. What does it identify? The intended recipient of the packet — never the sender. Who chooses it? The recipient, not the sender. Each endpoint selects the Connection IDs by which it wishes to be addressed and advertises them to its peer (first in the Source Connection ID of the packets it sends, later in NEW_CONNECTION_ID frames) and the peer copies one of those into the Destination Connection ID of every packet it sends back ([RFC 9000 §5.1]). So on a packet the client sends, the DCID holds a value the server chose (it names the server) and the SCID holds a value the client chose (it names the client). The two fields always name recipient and sender, respectively, not "the client's ID" and "the server's ID" in any fixed way.

The one exception is the client's very first packet, when it has not yet heard from the server and so has no server-chosen CID to use. For that first Initial the client generates its own Destination Connection ID (an unpredictable value of at least 8 bytes) and reuses it until the server replies ([RFC 9000 §7.2]). That throwaway value is also what seeds the Initial keys (§2.2); this is precisely what "the client's Destination Connection ID" means in key derivation: the random bootstrap value the client placed in the destination field before it knew the server's, not a CID that names the client. As soon as the server responds, the client adopts the server's Source Connection ID as its Destination Connection ID and never uses the random one again.

Connection IDs also serve deployments: a load balancer can encode routing information in the CID it hands out, so later packets carrying that CID are steered to the right server without deep inspection.

Worked example: coalescing during the handshake #

Because the Initial and Handshake packet number spaces are separate, a server often has an Initial packet (finishing its side of the crypto) and a Handshake packet ready at the same moment. QUIC lets it coalesce them: concatenate both packets into one UDP datagram, long headers back to back, and send them together ([RFC 9000 §12.2]). The receiver processes each packet in turn, using the header of each to find its boundaries.

One constraint governs early datagrams: any UDP datagram carrying an Initial packet must be at least 1200 bytes, padded with PADDING frames if necessary ([RFC 9000 §14.1]; the value lives in constants.md §6 as the floor for max_udp_payload_size). This guarantees the path can carry a minimally sized QUIC packet before the server commits resources, and it underpins the anti-amplification limit of §8.1.

Note

Coalescing is not the same as multiplexing. Multiplexing puts frames from different streams into one packet's payload; coalescing puts whole packets (possibly from different number spaces) into one datagram. A single datagram can do both at once.

In practice

editorial Choosing a Connection ID length is a real deployment decision. Longer CIDs give load balancers more room to encode routing and server identity but cost header bytes on every packet; 8 bytes is a common middle ground. Whatever the scheme, servers must be able to recognize and route the CIDs they issue for the life of the connection, including after migration. Get this wrong and roaming clients silently break. See §13.1.

Takeaways #

A QUIC packet is a small container: a header that names the version and connection, then a protected payload of frames drawn from any mix of streams. Long headers bootstrap; short headers carry steady-state traffic; Connection IDs free the connection from its address. This closes the foundational picture. §1.5 turns it into practice by capturing and decrypting a real handshake so every field above can be seen on the wire.