QUIC Connection IDs are not just opaque identifiers; they are the linchpin for how QUIC connections persist across network changes and how middleboxes can potentially route traffic without breaking end-to-end encryption.

Let’s see this in action. Imagine a client initiating a QUIC connection. The server, upon receiving the initial Initial packet, assigns a set of Connection IDs to the client. The client, in turn, uses these IDs to send subsequent packets.

Client -> Server: Initial packet (Src CID: 0, Dst CID: 1)
Server -> Client: Handshake packets (Src CID: 1, Dst CID: 0, New Dst CID: 2)
Client -> Server: Handshake packets (Src CID: 0, Dst CID: 2)

Here, the server not only responds but also introduces a new Destination Connection ID (CID 2). The client acknowledges this by sending its next packet using this new CID. This is fundamental: the server can now receive packets on CID 2, even if the client’s IP address or port changes.

The core problem QUIC Connection IDs solve is the fragility of UDP-based connections when faced with Network Address Translators (NATs) or load balancers. Traditional UDP sockets are identified by the 5-tuple (source IP, source port, destination IP, destination port, protocol). If any element of this tuple changes, the connection is effectively broken, and a new one must be established. This is a major pain point for mobile clients that frequently switch networks (Wi-Fi to cellular, or between Wi-Fi access points).

QUIC’s Connection IDs decouple the connection from the 5-tuple. Each QUIC endpoint can support multiple CIDs. When a client’s IP address or port changes, it can simply start sending packets using an existing Connection ID that the server already knows. The server, upon receiving a packet with a known CID but a new source IP/port, can update its internal mapping without terminating the connection.

This is achieved through the NEW_CONNECTION_ID frame. When a server wants to allow the client to use a new CID (perhaps because it anticipates the client might change IP/port, or for load balancing reasons), it sends this frame. The client then adds this new CID to its set of active CIDs.

Consider a client on Wi-Fi (192.168.1.100:12345) connecting to a server (203.0.113.1:443). The initial connection uses CID A for the client and CID B for the server.

Client (192.168.1.100:12345, CID A) -> Server (203.0.113.1:443, CID B)
Server (203.0.113.1:443, CID B) -> Client (192.168.1.100:12345, CID A)

Now, the server wants to migrate the client to a new server instance or prepare for the client’s potential network change. It sends a NEW_CONNECTION_ID frame:

Server (203.0.113.1:443, CID B) -> Client (192.168.1.100:12345, CID A):
  NEW_CONNECTION_ID frame {
    SeqNum: 1,
    CID: C,
    RetirePriorTo: 0,
    StatelessResetToken: <token_for_C>
  }

The client now has CID C available. If the client switches to cellular (10.0.0.5:54321), it can immediately start sending packets using CID C:

Client (10.0.0.5:54321, CID C) -> Server (203.0.113.1:443, CID B)

The server receives this packet. It recognizes CID C, looks up its associated StatelessResetToken (which it sent earlier), and verifies it. It then knows that the client has migrated and updates its internal state to associate CID C with the new IP/port 10.0.0.5:54321. The connection persists.

This mechanism also enables seamless migration from the client’s perspective. A client can proactively initiate a migration by generating a new CID and sending an NEW_CONNECTION_ID frame to the server, then start sending subsequent packets using that new CID, even before the server has processed the NEW_CONNECTION_ID frame. This allows the client to switch networks and inform the server of the change in a single round trip.

The RetirePriorTo field in the NEW_CONNECTION_ID frame is crucial for managing the lifecycle of CIDs. When a CID is retired, it means that the endpoint will no longer accept packets on that CID. This prevents old, potentially compromised, or no-longer-needed CIDs from being used indefinitely.

The most surprising aspect of QUIC Connection IDs is how they enable middleboxes, like load balancers, to participate in routing without needing to decrypt traffic. A load balancer might see incoming UDP packets on port 443. It can inspect the Destination Connection ID. If it’s a known CID that it’s responsible for routing to a specific backend server, it can forward the packet to that backend without needing to inspect the payload. This is a significant departure from HTTP/2 over TLS, where middleboxes often struggled with TLS session state.

The StatelessResetToken is a critical security feature. It’s a token generated by the sender of a NEW_CONNECTION_ID frame, which the receiver must keep. If an endpoint receives a packet on a CID it knows, but the StatelessResetToken provided in the packet doesn’t match the one it generated for that CID, it can discard the packet, preventing certain denial-of-service attacks.

The next hurdle you’ll encounter is understanding how these Connection IDs are managed during the handshake and how they interact with the transport layer’s congestion control and stream multiplexing.

Want structured learning?

Take the full Quic course →