Internet Protocols · QUIC and HTTP/3
Connection IDs, Migration, and Validating a New Path
Why QUIC addresses packets by connection rather than by four-tuple, how NEW_CONNECTION_ID hands out routable identifiers, and what PATH_CHALLENGE proves about a peer that has moved.
A TCP connection is identified by the four-tuple of source address, source port, destination address, and destination port. Change any element and the connection is gone. A phone moving from Wi-Fi to a cellular network changes all four at once, and every TCP connection on it fails, which is why a download interrupted by a network switch has to start over. QUIC separates the identity of a connection from the addresses currently carrying it, and that separation is the reason connection IDs exist at all.
The four-tuple is a routing hint, not an identity
Every QUIC packet carries a destination connection ID, and the receiver looks up its state by that value. Source and destination addresses still matter for routing the datagram, and the receiver records them to know where to send replies, but they are not what the connection is keyed on. A packet arriving from a new address with a valid connection ID and valid authentication is a packet for an existing connection, not a new one.
This has a direct consequence for how a server scales. A load balancer in front of a pool of servers cannot inspect the encrypted payload to decide which server owns a connection, and the four-tuple may change mid-connection. It can instead use the connection ID: if the ID encodes or references the server that owns the state, then any server in the pool can route the packet by reading the clear-text prefix of the header. That is why servers often choose long connection IDs while clients choose short ones or none at all. A client behind a NAT gains nothing from a long ID and pays bytes on every packet; a server behind a load balancer needs the ID to be a routing key.
The specification permits a zero-length connection ID, and it imposes a matching obligation: an endpoint that chooses a zero-length ID for its peer to use cannot require the peer to include a destination connection ID that identifies it, so the peer sends nothing in that field, and the receiver must then use the four-tuple to demultiplex. An endpoint using a zero-length ID gives up migration on its own side, because after a move there is nothing in the packet that ties it to the old connection. The field itself sits in the clear-text header ahead of the protected bytes, in the layout described in packet and frame layout on the wire.
Issuing and retiring identifiers
NEW_CONNECTION_ID is the frame that hands the peer additional identifiers. It carries a sequence number, a retire-prior-to value, a one-byte length, the connection ID bytes, and a sixteen-byte stateless reset token. The sequence number orders the identifiers an endpoint has issued; retire-prior-to tells the peer that any identifier with a lower sequence number is no longer usable, which lets an endpoint rotate identifiers without waiting for acknowledgements of the retirement.
The peer’s obligation is bounded. It must retire an identifier once it has used a packet with a higher sequence number, once the retire-prior-to field covers it, or once it has received a RETIRE_CONNECTION_ID frame naming it. The frame is the peer’s acknowledgement that it will no longer send packets with that identifier, and it frees the issuer to forget the associated state. An endpoint must keep at least one usable identifier available to its peer at all times, and the active_connection_id_limit transport parameter, whose minimum permitted value is 2, bounds how many the peer will store.
Rotation exists because an identifier that stays fixed for the life of a connection becomes a stable tracking handle. An observer on the path can follow a client across networks by watching one value, and an identifier that appears in a clear-text header is visible to anything that can see the datagram. Rotating identifiers, and retiring old ones, limits how long any single value remains linkable.
The stateless reset token is the other half of the design. If a server loses its state, whether from a restart or from an eviction, it can no longer decrypt anything, and it cannot send a CONNECTION_CLOSE because it does not have the keys. It can, however, send a packet whose bytes look random but whose last sixteen bytes equal the token that was issued alongside the connection ID the client used. The client compares the trailing bytes of an unrecognisable packet against the tokens it holds and, on a match, treats the connection as closed. Without this, a client would keep retransmitting into a black hole until an idle timeout expired.
Proving the new path belongs to the same peer
Migration is not free, and the specification does not let an endpoint simply accept a packet from a new address and continue. A peer that receives a packet from an unfamiliar address must validate the path before sending substantial data to it, because the address may be spoofed. The validation is PATH_CHALLENGE, a frame carrying eight bytes of unpredictable data, sent to the new address; the peer must echo those bytes in PATH_RESPONSE, and only then is the path confirmed.
The eight-byte payload is what makes the test meaningful. It cannot be predicted from anything an off-path attacker can see, and the response must arrive at the address the challenge was sent to, which proves that the endpoint named in the source address is reachable and is the same endpoint that holds the connection keys. Until the response arrives, the sender is subject to an anti-amplification limit on the new path: no more than three times the bytes received from that address.
Congestion control also restarts. A new path has unknown capacity, and the old path’s estimates do not transfer, so the congestion window and round-trip estimate are reset for the new path. An endpoint that has migrated keeps its connection-level state — stream data, flow-control credit, keys — but its notion of the network is rebuilt. That is the cost of mobility: the connection survives, the performance model does not.
What migration does not do
Migration is not multipath. An endpoint may probe several paths at once, but those probes carry only PATH_CHALLENGE, PATH_RESPONSE, NEW_CONNECTION_ID, and PADDING; a packet containing any other frame is what declares a path as the one carrying data. The base protocol keeps a single congestion control context and a single loss recovery context, so there is no way to send a stream across two paths in parallel. Multipath QUIC has been the subject of separate proposals precisely because the base protocol does not provide it.
Migration is also constrained by what the peer will accept. A server that has chosen a zero-length connection ID cannot migrate, because the client has no identifier to address it by after the address changes. More generally, an endpoint that does not issue new connection IDs, or that issues them without stateless reset tokens, is limiting what the peer can do after a disruption.
The client’s first flight has an additional wrinkle. It uses a destination connection ID chosen at random, because it has not yet learned one from the server. The server echoes that value as its source connection ID in its first response, so the client addresses all its later packets to the value the server chose, not to the random one it invented. Until the handshake is confirmed, the server’s packets travel to whatever identifier the client selected, and a server that chose a zero-length ID sends no identifier at all, which is one reason migration and zero-length identifiers do not mix.
Where the path is measured
The path MTU is not known in advance, and QUIC cannot rely on IP fragmentation. Implementations probe by sending packets of increasing size and watching whether they are acknowledged, falling back when a size produces no response, and treating the result as a property of the current path rather than of the connection. A migration invalidates the measurement along with the congestion state.
The datagram size also interacts with the anti-amplification limit. During validation the sender can send at most three times what it has received on the new path, so a server that wants to respond with a large amount of data after a client migrates must wait for the client to send enough, or for the PATH_RESPONSE to arrive and lift the restriction. This is why migration is cheapest for a client that has data to send: its own traffic pays for the server’s replies.
None of this is visible to the application. A stream that was mid-transfer when the path changed continues, with the same offsets and the same flow-control state, and the retransmission of anything lost during the transition happens through the same mechanism described in loss detection without retransmitted packets. What the application observes, if it observes anything, is a pause of roughly one round trip on the new path.