Skip to main content

INITIAL_BIDI_STREAMS

Constant INITIAL_BIDI_STREAMS 

Source
pub const INITIAL_BIDI_STREAMS: u32 = 16;
Expand description

Bidirectional streams a peer may have open before it has authenticated.

[limits] max_streams_bidi is what an authenticated connection is worth — one stream per tunnel, 1024 of them by default — and it used to be what the transport parameters advertised at the handshake, which handed it to every peer that could complete one. So a client that had proved nothing could open 1024 request streams at once and draw a 407 on each: 1024 parked request tasks and 1024 refusals written for a peer with no credentials. Neither bound that already applied there is a bound on concurrency — D77’s 1 MiB HEADERS budget bounds bytes buffered, D76’s absolute deadline bounds time — so nothing measured the one quantity that was free.

It is also work this process pays for at the wrong moment. quinn reserves a stream slot for every unit of the allowance when the connection is created, not when a stream is opened (see docs/configuration.md), so the configured value was a per-handshake cost that a peer never had to authenticate to impose. Clamping moves it to the first request that gets past the door.

Sixteen, the same number as MAX_PEER_UNI_STREAMS and for the same kind of reason: margin over what the protocol needs rather than the minimum, because running into a transport parameter is a QUIC-level stall with no application-level explanation attached. It happens to be exactly D77’s budget as well — sixteen field sections at the 64 KiB per-frame cap is the 1 MiB a connection may buffer — so what an unauthenticated peer can hold in HEADERS is now bounded twice over by the same number.

§Why it cannot stall a client that bursts

A client may fire several CONNECTs the moment the handshake completes, before the first 200 comes back, and more than sixteen of them would find the seventeenth blocked rather than refused — STREAMS_BLOCKED, which is backpressure and not an error. What unblocks it is the first of the sixteen to authenticate, and that is decided before any name is resolved and any socket is opened (conn::handle_request), so the raise happens in the same task that read the first HEADERS and its MAX_STREAMS rides the packet carrying that request’s response. The seventeenth tunnel therefore waits the round trip the client was already waiting for its first answer, once per connection, and only on a connection that opens more than sixteen tunnels before any of them is answered. Below that this is never reached at all.

Closing streams returns credit before authentication too, which is what keeps a well-behaved unauthenticated peer moving at all — RFC 9000 §4.6 leaves that to the receiver (“this document leaves implementations to decide when and how many streams should be advertised to a peer via MAX_STREAMS. Implementations might choose to increase limits as streams are closed”), and quinn announces one only once an eighth of the window has come back, so three closes rather than one. That is why the raise is what a burst waits on and not the churn.

Which is also why the raise is not conditioned on hearing from the peer. The same section: “An endpoint MUST NOT wait to receive this signal before advertising additional credit, since doing so will mean that the peer will be blocked for at least an entire round trip, and potentially indefinitely if the peer chooses not to send STREAMS_BLOCKED frames.”