Why a QUIC connection survives a client IP change: connection IDs, path validation, and what the load balancer has to do

QUIC ties packets to a connection by connection ID, not address. From RFC 9000: path validation when the client address changes, why each path needs a new connection ID, and load balancers.

quicrfc-9000networkprotocol

A QUIC server log can show packets for one connection suddenly arriving from a different IP address or port. The client may have moved to another network, or a NAT on the way may have picked a new outside port. QUIC is designed to keep the connection going in both cases. It does not happen for free, though: the receiver has to check the new address, and any load balancer in front of the server has to send those packets to the same server.

This post follows RFC 9000 (May 2021), which defines QUIC version 1: sections 5.1, 5.2, 8, 9, 17.2–17.3, 18.2 and 19.15–19.18 (checked 2026-10-19). It does not cover when a particular QUIC implementation, browser or operating system actually starts a migration, or any extension outside RFC 9000.

The key is the connection ID, not the address

A QUIC connection can have several connection IDs, and any of them identifies the connection. The important detail is that each endpoint chooses the connection IDs its peer will use. The Destination Connection ID on packets from client to server is a value the server issued, and the reverse direction uses values the client issued (section 5.1).

RFC 9000 states the primary purpose of connection IDs directly: changes in addressing at lower layers (UDP, IP) must not cause packets for a QUIC connection to be delivered to the wrong endpoint. If a received packet has a non-zero-length Destination Connection ID that belongs to an existing connection, it is processed as part of that connection, whatever address it came from (section 5.2). The first sentence of section 9 says the same thing: using a connection ID lets a connection survive changes to endpoint addresses (IP address and port).

A few format rules are worth knowing:

  • The short header used by 1-RTT packets (section 17.3) carries only the Destination Connection ID, with no length field. The receiver has to know the length already. RFC 9000 says endpoints behind a load balancer that routes on connection ID could agree with the load balancer on a fixed length, or on an encoding where a fixed portion carries the length (section 5.1).
  • In QUIC version 1 a connection ID is at most 20 bytes (section 17.2). A connection ID sent in a NEW_CONNECTION_ID frame must be 1 to 20 bytes (section 19.15).
  • A connection ID MUST NOT contain information that lets an external observer (one not cooperating with the issuer) correlate it with other connection IDs for the same connection. The same value MUST NOT be issued twice on one connection (section 5.1).

Zero-length connection IDs are allowed. The endpoint then has to tell connections apart by address, and RFC 9000 warns that multiplexing several connections with zero-length IDs on the same IP address and port fails once peer migration, NAT rebinding or client port reuse happens. An endpoint MUST NOT do that unless it is certain those features are not in use (section 5.1).

Who can migrate, and when

RFC 9000 separates two kinds of address change (section 9):

Kind What happens Intended by the endpoint?
Active migration The endpoint starts sending from a new local address Yes
NAT rebinding A middlebox, usually a NAT, assigns a new outgoing port or IP address to the flow No

In either case, a receiver that detects any change to its peer’s address MUST run path validation, unless it has already validated that address.

Migration has limits:

  • An endpoint MUST NOT start a migration before the handshake is confirmed. RFC 9000 points to section 4.1.2 of RFC 9001 for the definition of “confirmed”.
  • This version of QUIC only allows migration to new client addresses. Clients start all migrations. A client that receives packets from an unknown server address MUST discard them. The one exception is the server’s preferred address, covered below.
  • If the server sent the disable_active_migration transport parameter, the client MUST NOT send packets, probing packets included, from a new local address to the address the server used during the handshake.

Probing and non-probing packets

The migration rules depend on a distinction from section 9.1:

Class Frames
Probing frames PATH_CHALLENGE, PATH_RESPONSE, NEW_CONNECTION_ID, PADDING
Non-probing frames Every other frame (STREAM, ACK and so on)

A packet with only probing frames is a probing packet; a packet with any other frame is a non-probing packet. Before migrating, an endpoint MAY test a new local address with probing packets. If that validation fails, the new path simply cannot be used for this connection; the connection does not end while other valid paths remain.

The migration itself happens when an endpoint sends packets with non-probing frames from the new local address (section 9.2). From the receiver’s side, a packet with a non-probing frame from a new peer address means the peer has moved to that address (section 9.3).

What the server does when the client address changes

Here is a client moving from address A to address B, following RFC 9000’s rules. The connection ID names and the order of events are only for illustration.

Client (A → B)                            Server
|-- from B: STREAM etc., DCID=s2 ------->|  non-probing packet from new address B
|                                        |  → send further packets to B
|<-- to B: PATH_CHALLENGE(x), DCID=c2 ---|  start validating B; until then send
|                                        |    at most 3x the bytes received from B
|<-- to A: PATH_CHALLENGE(y), DCID=c1 ---|  also validate the old path A
|-- from B: PATH_RESPONSE(x), DCID=s2 -->|  same 8 bytes come back → B validated
|                                        |  reset congestion control and RTT estimate
|                                        |    (may keep them if only the port changed)

s2: issued by the server. Unused before the client's first packet from B;
    the client uses it on what it sends from B.
c2: issued by the client. Unused before the server's first packet to B;
    the server uses it on what it sends to B.
c1: issued by the client. Already associated with path A: the server used it
    toward A before the move. c2 is now used toward B, so it cannot also be
    used toward A (section 9.5).

The requirements at each step:

Send to the new address and start validating it. If the receiver permits the migration, it MUST send subsequent packets to the new peer address and MUST start path validation to check that the peer owns that address, unless validation is already underway (section 9.3). Because packets can arrive out of order, the send address only changes in response to the highest-numbered non-probing packet.

Send little before validation. An attacker with a spoofed source address could use migration to make an endpoint flood an unsuspecting victim (section 9.3.1). So an endpoint MUST NOT send more than three times the amount of data it has received from an unvalidated address (section 8). This is the anti-amplification limit.

PATH_CHALLENGE and PATH_RESPONSE. The validating endpoint sends a PATH_CHALLENGE carrying 8 unpredictable bytes (MUST), and the receiver MUST echo the same value in a PATH_RESPONSE (sections 8.2.1, 8.2.2, 19.17 and 19.18). The response MUST go out on the path where the challenge arrived, and MUST NOT be delayed except by congestion control. The 1200-byte rule is worded differently for the two frames:

  • A datagram carrying a PATH_CHALLENGE MUST be expanded to at least 1200 bytes, unless the path’s anti-amplification limit does not permit sending a datagram that large (section 8.2.1). Sending datagrams of that size checks that the path from the endpoint to its peer can be used for QUIC.
  • A datagram carrying a PATH_RESPONSE MUST also be expanded to at least 1200 bytes, but MUST NOT be expanded if the result would exceed the anti-amplification limit (section 8.2.2). RFC 9000 expects that to happen only when the PATH_CHALLENGE itself was not sent in an expanded datagram.

Validation succeeds only when a PATH_RESPONSE with the data that was sent comes back. If the PATH_CHALLENGE went out in a datagram smaller than 1200 bytes, a matching response still validates the peer’s address, so the endpoint can now send more than three times what it has received. The path MTU, however, is not validated, and the endpoint MUST start another path validation with a PATH_CHALLENGE in an expanded datagram to check that the path supports the required MTU. An acknowledgment of the packet that carried the PATH_CHALLENGE does not count, because acknowledgments can be spoofed (all section 8.2.3).

Check the old path too. An off-path attacker who can see packets can forward copies of genuine ones so they arrive first. That looks like a NAT rebinding and can move the connection onto a path through the attacker. To counter this, an endpoint that sees an apparent migration MUST validate the previously active path with a PATH_CHALLENGE (section 9.3.3). A non-probing packet with a higher packet number arriving on the original path moves the connection back.

Fall back on failure. Path validation only fails when the validating endpoint gives up. It SHOULD give up based on a timer, and RFC 9000 recommends three times the larger of the current PTO and the PTO for the new path, the latter computed with kInitialRtt from RFC 9002 (section 8.2.4). When validation of a new peer address fails, the endpoint MUST revert to the last validated peer address. If it has no state about one, it MUST close the connection silently by discarding all connection state (section 9.3.2).

Reset congestion control. The new path may not have the same capacity as the old one. Once the endpoint confirms the peer owns the new address, it MUST reset the congestion controller and RTT estimator for the new path to initial values. The exception is a change of port number only: that is usually NAT rebinding or other middlebox activity, so the endpoint MAY keep its congestion state and RTT estimate (section 9.4).

After verifying a new client address, the server SHOULD send the client new address validation tokens (section 8; stated in section 9.3).

A new path needs a new connection ID

Using one connection ID on several paths would let a passive observer link the traffic on those paths to one connection. RFC 9000 therefore says (section 9.5):

  • An endpoint MUST NOT reuse a connection ID when sending from more than one local address, whether it is migrating or only probing a new path.
  • An endpoint MUST NOT reuse a connection ID when sending to more than one destination address.
  • One exception: if network changes outside the peer’s control (NAT rebinding, for example) bring packets with the same Destination Connection ID from a new source address, the endpoint MAY keep using its current connection ID toward the new remote address, as long as it still sends from the same local address.

Where this bites is the supply of connection IDs. In the diagram, c2, which the server uses toward B, must be a value the client issued in advance. An endpoint that has run out of connection IDs cannot probe new paths or start a migration, and cannot respond to its peer’s probes or migration either (section 9.5). If the receiver has no unused connection ID from the peer, it cannot send anything on the new path until the peer provides one (section 9.3).

The supply is managed with these:

Mechanism What it does Section
active_connection_id_limit transport parameter The most connection IDs from the peer that an endpoint will store. MUST be at least 2; 2 if absent 18.2
NEW_CONNECTION_ID frame Delivers a new connection ID with its sequence number and 16-byte stateless reset token. Its Retire Prior To field asks the peer to retire IDs with lower sequence numbers 19.15, 5.1.2
RETIRE_CONNECTION_ID frame Says the sender will no longer use an ID its peer issued, and also asks for new ones 19.16, 5.1.2

The issuer SHOULD make sure its peer has enough available, unused connection IDs, and MUST NOT provide more than the peer’s limit. It SHOULD supply a new one when the peer retires one (section 5.1.1). An endpoint that starts a migration SHOULD also make sure the peer can use a new connection ID on migration. If the peer might have run out, RFC 9000 suggests including a NEW_CONNECTION_ID frame in every packet sent on the new path (section 9.5).

Servers behind a load balancer

A load balancer that picks a server only from source and destination IP addresses and ports may send a packet from a changed client address to a different server. Section 5.2.3 of RFC 9000 gives two ways for such a deployment to keep connections going:

  1. Use an out-of-band mechanism to forward packets to the right server based on connection ID.
  2. If servers can use a dedicated IP address or port, other than the one the client first connected to, use the preferred_address transport parameter to ask clients to move there. Clients may choose not to.

A server in a deployment with neither SHOULD signal that migration is not supported with the disable_active_migration transport parameter. Some points about this parameter:

  • What it forbids is the client sending from a new local address to the server address used during the handshake, that is, active migration (section 18.2). NAT rebinding does not involve the client using a new local address, so this prohibition cannot prevent it, and the section 9 rule to run path validation on any detected address change still applies. That reading is this post’s interpretation of the RFC text.
  • If a client breaks the rule, the server MUST either drop the packets on that path without sending a Stateless Reset, or go ahead with path validation and let the client migrate (section 9). Sending a Stateless Reset or closing the connection would let third parties close connections by spoofing or manipulating traffic.
  • The parameter does not prohibit migration after the client has acted on a preferred_address.
  • Server deployments using this simple address-based load balancing MUST avoid creating a stateless reset oracle (sections 5.2.3 and 21.11).

The server’s preferred address

Section 9.6’s preferred address is the only way to change the server address. RFC 9000 says it is particularly useful when clients first connect to an address shared by several servers but would prefer a unicast address for connection stability.

  • During the handshake the server can advertise one IPv4 and one IPv6 address in the preferred_address transport parameter. To offer only one family, it sends an all-zero address and port (0.0.0.0:0 or [::]:0) for the other (section 18.2).
  • The parameter carries a connection ID with sequence number 1 and its stateless reset token, so the client has at least one unused connection ID for the move. A server that chose a zero-length connection ID MUST NOT send a preferred address.
  • Once the handshake is confirmed, the client SHOULD pick one of the two addresses and start path validation, and it MUST validate the address before migrating (section 9.6.2). If validation fails, it MUST keep sending to the server’s original address.
  • The server MUST keep sending non-probing packets from its original address until it has received a non-probing packet from the client at the preferred address and has validated the new path.
  • A preferred address is only valid for the connection that advertised it. The client MUST NOT use it for other connections, including ones resumed from this connection.

Checklist for implementing or operating

Rules that come straight from RFC 9000, kept apart from this post’s own suggestions.

RFC 9000 rules

  • A packet from a changed address with a matching connection ID belongs to the same connection; confirm the new address with path validation.
  • Before validation, send no more than three times what was received from that address.
  • On an apparent migration, validate the old path with PATH_CHALLENGE too; if validation of the new address fails, go back to the last validated one.
  • Use an unused connection ID for each new local or destination address, and keep the peer supplied with unused IDs.
  • Behind a load balancer that cannot find the server by connection ID, send disable_active_migration.

This post’s suggestions

  • With active_connection_id_limit at its default of 2, the peer may hold just one spare besides the ID from the handshake. If your server serves clients that migrate or go through NAT rebinding often, check that value and how many IDs you actually issue.
  • In per-connection logs, record connection ID sequence numbers and path validation results, not only addresses. Then an address change on one connection can be sorted into migration, NAT rebinding or failed validation.

Sources

This post covers only the rules in RFC 9000. Link and content checked on October 19, 2026.

Back to the blog