Learning objective. Understand what transport parameters are, how they are encoded and carried in the handshake, and (the point that trips people up) that they are declarations, not a negotiation: each endpoint's parameters constrain what its peer may do.
Declarations, not a negotiation #
The single most important thing about transport parameters is stated plainly in RFC 9000:
"Transport parameters are declarations that are made unilaterally by each endpoint. Each endpoint can choose values for transport parameters independent of the values chosen by its peer." ([RFC 9000 §7.4])
There is no round of offers and counter-offers. Each side simply announces its own limits, and
those limits bind the other side. When the server declares initial_max_data = 1 MB, it is
telling the client "you may send me up to 1 MB of stream data before I extend more"; the client's
own initial_max_data is a separate declaration governing what the server may send. The two
are unrelated numbers. This asymmetry is the recurring source of confusion in the sections that
follow, so it is worth fixing now: a parameter you send is a limit on your peer, computed from
what you are willing to receive.
Because they ride inside the TLS handshake (the quic_transport_parameters extension), both
endpoints' parameters are authenticated by the time the handshake completes: "both endpoints
make authenticated declarations of their transport parameters" ([RFC 9000 §7.4]). An attacker
cannot tamper with them without breaking the handshake (Chapter 2).
The wire encoding #
Each parameter is a simple (ID, Length, Value) triple, with the ID and Length as variable-length integers ([RFC 9000 §18]):
The extension is a flat sequence of these triples. A few consequences fall straight out of the format:
- Unknown IDs are skippable. Read ID, read Length, skip Length bytes — the same
read-and-skip discipline as HTTP/3 frames (§5.1), which is what lets reserved
"GREASE" parameters (
0x1f·N + 0x1b,constants.md§6; note the transport-parameter base is0x1b, not HTTP/3's0x21) exercise a peer's forward-compatibility. - Zero-length values are meaningful flags.
disable_active_migration(0x0c) and other flag parameters carry Length 0; presence alone is the signal ([RFC 9000 §18.2]). - Duplicates are an error. An endpoint MUST NOT send a parameter twice, and a receiver
SHOULD treat a duplicate as
TRANSPORT_PARAMETER_ERROR(0x08); so is any value out of its valid range ([RFC 9000 §7.4]).
Who may send what #
The registry (constants.md §6) splits into parameters either side may send and a handful that
are server-only, because they exist to authenticate the handshake itself. A client MUST NOT
send original_destination_connection_id, stateless_reset_token, preferred_address, or
retry_source_connection_id, and a server that receives any of them MUST close with
TRANSPORT_PARAMETER_ERROR ([RFC 9000 §18.2]).
That connection-ID group does real security work. Each endpoint echoes the connection IDs it used
during the handshake back through transport parameters — the client its
initial_source_connection_id, the server the original_destination_connection_id (the DCID of
the client's first Initial) and, if it sent a Retry, the retry_source_connection_id
([RFC 9000 §7.3]). Because these values are inside the authenticated handshake, "an attacker
cannot influence the choice of connection ID for a successful connection by injecting packets"
during the handshake ([RFC 9000 §7.3]). A missing or mismatched value here is fatal to the
connection — which is exactly the intent.
The 0-RTT wrinkle #
Transport parameters interact with 0-RTT in a way worth flagging early (§2.3). When a
client sends 0-RTT data, it has not yet seen the server's current parameters, so it must use the
ones it remembered from the previous connection ([RFC 9000 §7.4.1]). To keep that safe, a
server accepting 0-RTT MUST NOT reduce the limits the client is relying on. initial_max_data,
the initial_max_stream_data_* limits, the stream-count limits, and active_connection_id_limit
may only stay the same or grow across the resumption ([RFC 9000 §7.4.1]). Certain other
parameters (ack_delay_exponent, max_ack_delay, the connection-ID parameters, …) are never
remembered and must be taken fresh from the new handshake. §7.2 and §7.3 return to the specific
limits; the rule to carry forward is that 0-RTT freezes the receive limits a server advertised
last time.
Worked example: reading a parameter block #
Suppose a server's extension contains 01 04 80 00 75 30 0e 01 04 0c 00. Parsed as triples:
01/len 04/value 80 00 75 30 is max_idle_timeout, a 4-byte varint decoding to 30000, so
30 s (§7.3); 0e/len 01/value 04 is active_connection_id_limit = 4
([RFC 9000 §18.2], default 2, minimum 2); 0c/len 00 is disable_active_migration, a flag with
no value (§10.1). Three declarations, read left to right, each one a limit the server
is placing on the client.
The max_udp_payload_size parameter (0x03) is a common misread: it is "a property of the
endpoint and not the path" ([RFC 9000 §18.2]). It is the largest UDP payload the endpoint is willing
to receive, defaulting to 65527 and invalid below 1200. It is not the path MTU and does not set
the datagram size QUIC actually sends; that is governed by PMTU discovery and the 1200-byte
Initial floor ([RFC 9000 §14.1]).
editorial When debugging a "why won't this connection do X" problem, dump both endpoints' transport parameters first and read each one as a limit on the other side. Most early-connection stalls (can't open a stream, can't send past the first few KB, connection dies after N seconds) are one endpoint bumping into a limit the peer declared and the operator never looked at. Keysight/Wireshark and every QUIC library's qlog surface the parsed extension; make reading it the first reflex, before you suspect congestion control or the application.
Takeaways #
Transport parameters are unilateral, authenticated declarations exchanged in the TLS handshake, each encoded as an (ID, Length, Value) triple, and each one a limit that binds the peer rather than the sender. A server-only subset ties the connection IDs into the authenticated handshake to prevent injection, and 0-RTT forces a server to remember and not shrink the receive limits a client depends on. With the mechanism understood, §7.2 turns to the parameters that matter most day to day (the stream-count and flow-control limits) and how to choose them.