QUIC packets are made of frames, and understanding these frames is like knowing the alphabet of the internet’s new language.

Let’s see QUIC in action, specifically how different frame types manifest in actual network traffic. Imagine a simple HTTP/3 request.

# Simulate a QUIC connection and HTTP/3 request
# This is a conceptual example; actual packet capture requires running a QUIC server/client.
# We'll describe the frames that would be observed.

# Client initiates connection:
# - Initial packet: Contains CONNECTION_CLOSE frame if handshake fails early, or HANDSHAKE frames.
# - During handshake: CRYPTO frames carrying TLS handshake data.
# - After handshake: VERSION_NEGOTIATION frame if versions don't match.

# Client sends HTTP/3 request:
# - Headers: HEADERS frame carrying HTTP headers.
# - Data: STREAM frame(s) carrying the request body.
# - Settings: SETTINGS frame for initial configuration.
# - Push Promise (optional): PUSH_PROMISE frame.

# Server responds:
# - Headers: HEADERS frame with HTTP response headers.
# - Data: STREAM frame(s) with response body.
# - Acknowledgment: ACK frame to confirm received frames.
# - PING: PING frame to keep connection alive.

A QUIC packet is a container. Inside, you’ll find one or more frames. Each frame type has a specific purpose, defining what kind of data or control information is being sent.

Here’s a breakdown of the core QUIC frame types you’ll encounter:

  • PADDING: This frame is simple: it contains zero or more bytes of data. Its primary use is to pad a packet to a specific size, often for security (obfuscation) or to meet network MTU requirements. When you see a PADDING frame, it’s just filler.

  • CONNECTION_CLOSE: This frame signals that a QUIC connection is being terminated. It includes a reason code and a reason phrase, providing context for why the connection is closing. For instance, 0x0001 might mean "NO_ERROR", while 0x0008 could indicate "PROTOCOL_ERROR".

  • APPLICATION_CLOSE: Similar to CONNECTION_CLOSE, but this frame is used to close the application-level portion of the connection (e.g., an HTTP/3 stream) while the underlying QUIC connection might remain open for other streams. It also carries a reason code and phrase.

  • MAX_DATA: This frame advertises the maximum amount of data the sender is willing to receive on the entire connection. It’s a flow control mechanism, preventing one side from overwhelming the other with data. When a receiver sends MAX_DATA, it’s telling the sender, "You can send me up to X more bytes of application data on this connection."

  • MAX_STREAM_DATA: This is the stream-specific counterpart to MAX_DATA. It advertises the maximum amount of data a sender is willing to receive on a particular stream. This allows for finer-grained flow control, enabling some streams to receive more data than others.

  • STREAM_DATA_BLOCKED: Sent by a receiver when it cannot accept more data on a specific stream due to flow control limits. It indicates that the sender should stop sending data on that stream until a MAX_STREAM_DATA frame is received.

  • STREAM_FRAME: This is the workhorse for carrying application data. It’s associated with a specific stream ID and can contain actual payload bytes. Importantly, it has a FIN bit. If set, it signifies that this is the last data frame for that stream.

  • HEADERS_FRAME: Used to carry HTTP headers or other header-like information. In HTTP/3, these frames are crucial for transmitting request and response headers.

  • SETTINGS_FRAME: This frame is used during the handshake to convey connection-wide parameters from one peer to another. Examples include SETTINGS_MAX_CONCURRENT_STREAMS (how many streams can be open simultaneously) or SETTINGS_INITIAL_MAX_DATA (initial connection-level flow control window).

  • PING_FRAME: A simple frame used to probe the liveness of the connection. If a PING frame is sent and not acknowledged within a reasonable time, it can indicate that the peer has become unresponsive.

  • ACK_FRAME: This is how acknowledgments are sent. It’s vital for reliability. An ACK frame contains a list of acknowledged packet numbers and potentially ranges of those numbers, along with information about received data (like ACK_ECN for ECN counts).

  • CRYPTO_FRAME: Carries cryptographic handshake data, primarily during the TLS handshake. These frames are essential for establishing the secure tunnel over which other frames will eventually travel.

  • NEW_TOKEN_FRAME: Used by the server to send a new connection ID to the client. This is part of the connection migration mechanism, allowing clients to switch network paths without breaking the connection.

  • STOP_SENDING_FRAME: Sent by a receiver to indicate that it no longer wishes to receive data on a specific stream. This is often used when a stream is no longer needed or if there’s an error condition on that stream.

  • PATH_CHALLENGE / PATH_RESPONSE: These frames are used to validate that a specific network path between the client and server is functional. The server sends a PATH_CHALLENGE with random data, and the client responds with PATH_RESPONSE containing the same data.

The most surprising aspect of QUIC frames is how they’ve decoupled transport concerns from the underlying protocol. Unlike TCP, where SYN, ACK, FIN, and PSH flags are embedded within the TCP header itself, QUIC frames allow for a richer, more explicit set of control messages that can be extended and managed independently of the packet header. This flexibility is key to QUIC’s ability to handle features like connection migration and improved loss recovery.

When you examine a QUIC packet, you’re essentially looking at a serialized list of these frames. The packet header itself contains minimal information: a version, a connection ID, and a packet number. The real "protocol" is encoded within the frames that follow. For example, a server might send a MAX_DATA frame to increase the flow control window for a client, and then a STREAM_FRAME with a FIN bit set on a specific stream to signal the end of a response. The client, in turn, would send an ACK_FRAME to acknowledge receipt of these frames.

The interplay between MAX_DATA, MAX_STREAM_DATA, and STREAM_DATA_BLOCKED frames is a prime example of QUIC’s flow control. A receiver might send MAX_DATA to say "I can take 1MB more total data." Then, for a specific video stream, it might send MAX_STREAM_DATA saying "I can take 500KB more on this stream." If the sender pushes more than 500KB on that stream, the receiver will send STREAM_DATA_BLOCKED for that stream and MAX_DATA for the connection, effectively pausing data transmission until the sender has processed some data and sent updated MAX_DATA or MAX_STREAM_DATA frames.

Consider the ACK_FRAME. It’s not just a simple "I got it." It can acknowledge multiple packets, and critically, it includes ECN (Explicit Congestion Notification) counts. This means QUIC can signal congestion much more granularly than traditional TCP, allowing for more proactive and less disruptive congestion control. A single ACK_FRAME can contain several "acknowledgment blocks," each detailing a range of received packet numbers and the number of ECN-marked packets within that range.

The next logical step after understanding frame types is to dive into QUIC’s loss recovery mechanisms, which are heavily reliant on the precise information conveyed in ACK_FRAMEs and the timely retransmission of frames.

Want structured learning?

Take the full Quic course →