flowchart TD accTitle: From a MoQ object to network packets accDescr: A MoQ object is framed into stream bytes. Parts of the stream are carried in QUIC STREAM frames, which travel inside QUIC packets, UDP datagrams, and IP packets. These boundaries need not align. O["MoQ object: metadata and payload"] --> B["Bytes on a QUIC stream"] B --> F["STREAM frames: stream ID, offset, bytes"] F --> Q["QUIC packets: frames and packet protection"] Q --> U["UDP datagrams"] U --> I["IP packets across the network"]
Chapter 02
QUIC and WebTransport
On this page
Before we look at MoQ messages, let’s look at how their bytes travel. QUIC carries them over a secure connection with several independent byte streams. In a browser, we use WebTransport to work with those streams and with datagrams.
We’ll start with TCP/IP, then see what happens when packets go missing and how QUIC lets us stop sending old content. After that, we’ll look at WebTransport. The packet layouts and handshake are at the end; you can come back to those when you want the details.
A short introduction to TCP/IP
If you are already familiar with TCP/IP, TCP, and UDP, skip ahead to Why one ordered stream can hurt.
When a student connects to the lecture, their packets usually pass through several networks. A router forwards each packet toward its destination. IP, the Internet Protocol, provides the addresses and delivery rules that make this possible. An IP address identifies a network interface, such as the one the student is using to connect.
A packet contains a header, which describes how to handle it, and a payload, the data it carries. Layers nest: the payload of an IP packet can contain a UDP datagram, whose payload contains QUIC packets. The receiving software unwraps those layers.
An address gets traffic to a host. A port helps the host direct that traffic to the appropriate transport endpoint. A server may listen for QUIC on UDP port 443 while serving TCP connections on TCP port 443: the transport protocol is part of the distinction. DNS translates a name such as relay.example into addresses the client can try. The client then uses an address to try connecting to the relay.
| Term | What it means | In our lecture |
|---|---|---|
| IP address | An address for a network interface | The address the student uses to reach the relay |
| UDP or TCP port | Identifies a transport endpoint on a host | The relay listens for QUIC on a UDP port |
| Packet | A unit sent across a network | It may carry part of an audio or video object |
| Connection | The transport state shared by two endpoints | The student and relay exchange data over a QUIC connection |
| Protocol | Rules for interpreting messages | MOQT tells the relay how to read the student’s subscription request |
IP delivery is best effort. Packets can be lost, duplicated, reordered, or delayed. A full queue in a router is one reason for loss. Each link has a maximum transmission unit, or MTU: its largest IP packet size. The path MTU is the smallest link MTU along the route. Sending one large application message does not make the network carry it as one arbitrarily large packet. See the IPv6 specification for packet-size constraints.
TCP and UDP make different promises
TCP, the Transmission Control Protocol, gives an application an ordered byte stream. If bytes go missing, TCP tries to recover them before handing later bytes to the application. The application reads a continuous sequence, even though those bytes traveled in separate packets. Delivery can still fail if the connection breaks.
UDP, the User Datagram Protocol, preserves message boundaries but does not supply ordering or retransmission. The application receives individual datagrams that reach it. A protocol built above UDP can add reliability, security, and congestion control. QUIC does exactly that.
| Property | TCP | UDP | QUIC |
|---|---|---|---|
| Carried directly by | IP | IP | UDP |
| Application data | One ordered byte stream in each direction | Separate datagrams | Multiple ordered byte streams; optional datagrams |
| Repairs lost application data | Yes, while the connection remains viable | No | Stream data: yes. QUIC datagrams: no |
| Preserves application message boundaries | No | Yes | Streams: no. Datagrams: yes |
| Encryption built into this protocol | No; commonly combined with TLS | No | Yes; integrates TLS 1.3 |
| Congestion control | Yes | Not supplied by UDP itself | Yes, including datagram traffic |
With several streams on one connection, we can put camera video on one and captions on another. Missing video bytes then don’t force the captions to wait.
Delay, rate, and feedback
Bandwidth describes a path’s carrying capacity. Throughput is the rate actually achieved. Latency is delay; always name the interval being measured. Network delay and the delay from a camera capturing a picture to a screen displaying it are different measurements.
An acknowledgment, or ACK, tells a sender what transport data arrived. Round-trip time, or RTT, measures a trip to the peer and back. A sender uses feedback to estimate the path and detect possible loss. To find out whether those bytes became a picture, we need to check the player too.
Two limits can stop a sender:
| Limit | Question it answers | Typical response |
|---|---|---|
| Flow control | Can the receiver accept more stream bytes? | Wait for the receiver to advertise more capacity |
| Congestion control | How much traffic can the network sustain? | Adjust the amount and pace of traffic sent |
A slow reader and a congested network can both look like “sending stopped,” but they need different fixes. QUIC’s loss detection and congestion control are described in RFC 9002.
Why one ordered stream can hurt
Imagine sending camera video and captions through a single ordered byte stream. A packet containing video bytes goes missing. Later caption bytes arrive, but the receiver cannot expose them through that stream until the missing earlier bytes are recovered. This is head-of-line blocking.
For a file, waiting for the missing bytes makes sense: we want the whole file in order. For our lecture, the captions may still be useful while the camera video catches up. Putting both on one stream makes them wait together.
Independent streams
QUIC provides multiple reliable, ordered byte streams within one connection. Loss affecting one stream does not impose that stream’s delivery order on another. A stream still has its own ordering: later bytes in that same stream wait for missing earlier bytes.
A byte stream has no built-in message boundaries. If the publisher writes two objects, the receiver cannot assume two reads will return those two objects. MOQT supplies framing so the receiver can find where each object starts and ends. RFC 9000 §2.2 explains this distinction.
All those streams still share the connection’s bandwidth. If the network gets congested, they have less capacity to share.
QUIC applies flow-control limits to both individual streams and their combined data. A receiver that stops consuming one stream can therefore hold up other streams if the connection-wide limit is exhausted. See RFC 9000 §4.1 and RFC 9002 §7.
MoQ uses these streams to separate content that can be useful on its own. In the next chapter, we’ll see how publishers group that content.
What a lost packet holds up
Suppose camera bytes use stream A and captions use stream B. The offsets below are illustrative byte positions, not MoQ object IDs.
| Event | Stream A: camera | Stream B: captions |
|---|---|---|
| Packet 10 arrives | Bytes 0–99 are readable | No data yet |
| Packet 11 is lost | Bytes 100–199 are missing | No data in this packet |
| Packet 12 arrives | Bytes 200–299 wait behind the gap | Bytes 0–39 can be read |
| Missing data arrives in a later packet | Bytes 100–299 become readable in order | No need to wait for A’s repair |
Here, the captions can continue while the camera stream waits. If packet 11 had carried bytes from both streams, both would have a gap to fill. The sender may also slow down the whole connection in response to the loss.
Reliability can have an ending
Sometimes the viewer no longer needs the bytes we’re trying to recover. QUIC lets the sender reset a stream, and lets the receiver ask it to stop. We can use this to give up on old video and spend the connection’s capacity on a newer picture.
Suppose an old camera group is still incomplete when the publisher has a new group with everything needed to start decoding again. We could keep repairing the old group, or cancel it and move on. The application chooses when catching up matters more than seeing every picture.
Where datagrams fit
QUIC’s DATAGRAM extension provides messages that are not retransmitted by QUIC when lost. They share the connection’s security and congestion control. They do not promise arrival or ordering, and their size is constrained by the path and negotiated limits.
QUIC does not split a DATAGRAM frame across packets. If an application’s message is too large, it must choose another mapping or define its own fragmentation and reassembly. That also means deciding what to do when one fragment is missing. See RFC 9221 §5.
That can suit a small, replaceable sensor update. It is less convenient for a large video frame that cannot fit in a datagram. A protocol must define how content maps onto these tools; “uses UDP” alone says very little about reliability.
A QUIC datagram is also different from a UDP packet. QUIC adds its own framing, encryption, and congestion behavior on top of UDP.
Migration and multipath
Suppose a publisher has both cellular and satellite access. It could switch to cellular when satellite service becomes poor, or use both links at once. These are different transport capabilities.
Connection migration lets a QUIC connection move to a new network path while keeping the connection alive, subject to validation and the peer’s migration settings. Base QUIC sends application data on one path at a time. Migration can preserve a session when a device changes networks, though detecting and validating the new path can still interrupt delivery. See RFC 9000 §9.
The multipath extension allows one QUIC connection to use several paths simultaneously. Both endpoints must support and negotiate it. This is separate from the MOQT specification; we refer here to QUIC multipath draft-21, which is still awaiting RFC publication at the time of writing.
Having two links also creates a scheduling decision: which data should use which path? The extension leaves that policy to implementations. A path with spare capacity may have too much delay for an urgent frame. Sending successive parts of a stream over paths with different delays can also make later bytes wait for earlier ones. More capacity alone doesn’t ensure lower playback latency.
An application can instead open separate MoQ connections and decide which one carries each subscription. Those are separate sessions, rather than multiple paths inside one QUIC connection. The moq.dev article On a Boat discusses both approaches. Check the chosen implementation’s support before relying on either in a deployment.
Security belongs to the connection
Running over UDP does not mean sending plaintext. QUIC uses Transport Layer Security (TLS) 1.3, the security protocol also used by HTTPS, to authenticate the server and establish keys that protect packets. With a relay in the middle, the publisher-to-relay and relay-to-viewer connections have separate protection. Hiding the content from the relay itself requires another layer, which we will discuss in the security chapter. See RFC 9001 §4.
WebTransport is the browser doorway
Browser JavaScript does not generally get a raw QUIC socket. WebTransport exposes streams and datagrams to web applications. For the MOQT mapping described in this edition, the browser uses WebTransport over HTTP/3; native software can use QUIC directly.
| Layer, from the application toward the network | Native direct-QUIC path | Browser path used in this book |
|---|---|---|
| Content and requests | MoQ Transport | MoQ Transport |
| Browser session interface | Not needed | WebTransport over HTTP/3 |
| Multiplexed secure transport | QUIC, integrating TLS 1.3 | QUIC, integrating TLS 1.3 |
| Datagram transport | UDP | UDP |
| Network addressing and forwarding | IP | IP |
HTTP/3 is HTTP mapped onto QUIC. Using WebTransport over HTTP/3 does not mean making a new HTTP request for every media object. Once established, the WebTransport session exposes streams and datagrams for the application. See RFC 9114 and the WebTransport API specification.
WebTransport transports application bytes. It does not know that those bytes represent a subscription, an Opus packet, or a camera frame. A MoQ implementation still has to implement the protocol above it.
A fallback changes the tradeoffs
The moq.dev stack documents a WebSocket fallback. This can help reach networks where the preferred transport is unavailable. It does not preserve all of QUIC’s independent-stream behavior: TCP provides one reliable, ordered byte stream. When the fallback carries all its work over that stream, missing bytes can hold up later bytes belonging to unrelated work.
When testing a player, record which transport it actually used. Otherwise, you can spend a long time explaining “QUIC performance” while measuring a different path.
When the player won’t start, check the connection first, then the MoQ session, then the media. A client might reach the server but speak the wrong protocol version, or receive content its decoder can’t use.
Reading a QUIC packet
The rest of this chapter looks inside the transport. You can continue to Tracks, Groups and Objects now and return here when you want to read packet fields.
A QUIC frame is a transport instruction inside a QUIC packet. It is unrelated to a video frame, which represents a picture. A packet can carry several QUIC frames, including stream data from different streams.
| QUIC frame | What it carries | Why it matters |
|---|---|---|
STREAM |
Stream ID, byte offset, and data; optionally an end marker | Places received bytes within the right stream |
ACK |
Ranges of received packet numbers | Gives the sender delivery feedback |
CRYPTO |
Cryptographic handshake data | Establishes the connection’s security |
MAX_DATA / MAX_STREAM_DATA |
Connection / stream flow-control limits | Allows more stream bytes to be sent |
RESET_STREAM |
An error code and final size for an abandoned sending direction | Ends that direction without delivering every byte |
STOP_SENDING |
A request to stop a stream’s sending direction | Lets a receiver reject further work |
DATAGRAM |
An unreliable message, when the extension is negotiated | Carries data without stream ordering or retransmission |
For the full frame layouts, see RFC 9000 §19. The DATAGRAM frame is defined in RFC 9221.
Three numbers answer different questions: a packet number identifies a transmission within its packet-number space; a stream ID identifies a stream; a byte offset locates data within that stream. When lost stream data is sent again, it travels in a new packet, with a new packet number. It retains its position in the stream. See RFC 9000 §13.3 for how QUIC retransmits information.
This diagram shows the direct-QUIC stream mapping. One object can span many packets; one packet can contain bytes from more than one object. The receiver reconstructs stream bytes before the MoQ parser reconstructs objects. WebTransport adds its own session and stream framing above QUIC.
Inside the packet
The following view expands one illustrative IP packet. Boxes show containment, not bit widths. Here, one UDP datagram contains one QUIC packet; other valid arrangements can carry multiple QUIC packets in a datagram.
The header’s form depends on the packet type. A connection ID helps identify a connection; a stream ID appears inside a STREAM frame and identifies one stream within it. The frame’s data can hold only part of a MoQ object.
For 1-RTT packets on a QUIC v1 connection, the short-header layout is more compact:
| Field, in wire order | Size | Purpose |
|---|---|---|
| First byte | 8 bits | Header form, fixed bit, spin bit, reserved bits, key phase, packet-number length |
| Destination connection ID | Agreed length; possibly zero | Identifies the receiving connection |
| Packet number | 1–4 bytes | Encodes a packet number for reconstruction by the receiver |
| Protected payload | Remaining bytes | Encrypted frames and authentication tag |
Selected first-byte bits and the packet-number bytes have header protection. The payload has authenticated encryption. A packet capture therefore does not expose this logical frame view without the relevant keys. Long headers, used by Initial and Handshake packets, also carry a version and source connection ID. Read RFC 9000 §17 for the exact layouts and RFC 9001 §5 for protection.
Read the first byte, bit by bit
Here is the base QUIC v1 short header before header protection is applied. It assumes no extension changes these bits. Read the first row from the most significant bit to the least significant bit. The remaining rows show field order; their drawn widths are not proportional to their lengths.
| Bit 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|
| 0 Form |
1 Fixed |
S Spin |
00 Reserved |
K Key phase |
PP Packet-number length |
||
| Destination connection ID — length known from connection context; may be zero | |||||||
| Packet number — 1, 2, 3, or 4 bytes | |||||||
| Protected payload — encrypted frames and authentication tag | |||||||
PP encodes the packet-number length minus one: 00 means one byte; 11 means four. The spin bit supports optional passive latency measurement. The key-phase bit helps identify packet-protection key updates. The short header has no version field or connection-ID-length field; decoding requires connection context. See RFC 9000 §17.3.1.
The optional QUIC-bit extension, RFC 9287 §3, allows peers to vary the otherwise fixed bit after the required negotiation. The layout above follows RFC 9000 without that extension.
For example, first byte 0x41 is binary 01000001: short header, fixed bit set, spin zero, reserved zero, key phase zero, and a two-byte packet number. This is the unprotected value. You cannot apply that reading directly to a protected packet capture.
How variable-length integers work
Many QUIC frame fields use a variable-length integer, abbreviated varint. The first two bits say how many bytes belong to the integer; the remaining bits carry its value, most significant bytes first.
| Prefix bits | Total bytes | Value bits | Largest value |
|---|---|---|---|
00 |
1 | 6 | 63 |
01 |
2 | 14 | 16,383 |
10 |
4 | 30 | 1,073,741,823 |
11 |
8 | 62 | 4,611,686,018,427,387,903 |
A one-byte value of 37 is 0x25. The value 64 needs at least two bytes: 0x40 0x40. Remove the leading 01 length prefix, then interpret the remaining 14 bits as the value. This encoding is defined in RFC 9000 §16. Packet numbers use a different encoding; do not parse them as varints.
Decode one STREAM frame
A STREAM frame type is in the range 0x08–0x0f. Its low three bits control which fields follow:
| Flag | Mask | When set |
|---|---|---|
| OFF | 0x04 |
An Offset field is present; otherwise the offset is zero |
| LEN | 0x02 |
A Length field is present; otherwise data extends to the packet’s end |
| FIN | 0x01 |
These bytes end the stream’s sending direction |
| Type | Stream ID | Offset | Length | Data |
|---|---|---|---|---|
| Varint 0x08–0x0f |
Varint Always |
Varint Only if OFF = 1 |
Varint Only if LEN = 1 |
Bytes Application content |
Try this complete illustrative frame, shown after decryption:
| Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 |
|---|---|---|---|---|---|---|
| 0e | 02 | 00 | 03 | 61 | 62 | 63 |
| TypeSTREAM | Stream ID2 | Offset0 | Length3 bytes | Data“abc” | ||
| Frame metadataOFF = 1, LEN = 1, FIN = 0 | Application bytesStream offsets 0, 1, 2 | |||||
Every integer in this example fits in one byte. Stream 2 is a client-initiated unidirectional stream. These three bytes occupy offsets 0, 1, and 2; FIN is unset, so the frame does not end the stream. This is a QUIC example, not a valid MoQ message: abc is just recognizable test data. Exact frame rules appear in RFC 9000 §19.8, with stream IDs in §2.1.
The handshake, message by message
A handshake establishes the shared state needed for protected communication. This example shows a fresh QUIC connection with server authentication, without Retry, resumption, or packet loss. Each arrow represents a flight of messages; it need not fit in one packet.
sequenceDiagram
accTitle: A fresh QUIC connection using TLS 1.3
accDescr: The client sends ClientHello in Initial packets. The server sends ServerHello in Initial packets and its certificate and Finished in Handshake packets. The client verifies the server and sends Finished. The server confirms the handshake with HANDSHAKE_DONE.
participant C as Client
participant S as Server
C->>S: Initial: CRYPTO carrying ClientHello
S->>C: Initial: CRYPTO carrying ServerHello
S->>C: Handshake: encrypted extensions, certificate, CertificateVerify, Finished
Note over C: Verify server identity and handshake
C->>S: Handshake: CRYPTO carrying Finished
Note over S: Verify client Finished
S->>C: 1-RTT: HANDSHAKE_DONE
C->>S: 1-RTT: application frames
S->>C: 1-RTT: application frames
The handshake messages shown here travel in QUIC CRYPTO frames. QUIC performs packet protection rather than carrying TLS records as TCP-based TLS does. Initial, Handshake, and 1-RTT identify packet protection levels; they are not three separate connections. The server’s certificate authenticates the server, not the student’s permission to watch a lecture. See RFC 9001 §4.
Initial packets use publicly derivable protection keys, so a network observer can recover their contents. Handshake and 1-RTT packets use secret keys established through TLS. Do not confuse Initial packet protection with confidentiality; see RFC 9001 §5.
The drawing puts application traffic last for readability. Endpoints can begin sending 1-RTT data once the required keys are available; the client need not wait for HANDSHAKE_DONE before sending it. ACKs are omitted. Resumption can enable 0-RTT, or early data, with replay considerations that applications must handle. The complete flows appear in RFC 9000 §7.1.