§4.1
Chapter 04 · Stream Multiplexing and Flow Control for Performance

§4.1Stream Types and Stream Lifecycle Management

RFC 9000

Learning objective. Name the four QUIC stream types and how their IDs are assigned, and follow a stream through its lifecycle: the send and receive state machines, and the three ways a stream ends.

Four kinds of stream #

A QUIC stream is an ordered, reliable byte sequence, and the two low bits of its stream ID encode who opened it and whether it is two-way (constants.md §5, [RFC 9000 §2.1]):

Low 2 bits Type
0x00 client-initiated, bidirectional
0x01 server-initiated, bidirectional
0x02 client-initiated, unidirectional
0x03 server-initiated, unidirectional

Within a type, IDs are handed out in order: a client's bidirectional streams are 0, 4, 8, …; its unidirectional streams 2, 6, 10, …. Bidirectional streams carry data both ways (an HTTP/3 request/response, §1.3); unidirectional streams carry data one way (the HTTP/3 control and QPACK streams). Opening a stream is implicit and lazy. Sending or receiving a frame that names a higher-numbered stream of a type opens all lower-numbered streams of that type at once, so there is no separate "open" handshake ([RFC 9000 §3.2]).

Two halves, two state machines #

A bidirectional stream is really two independently-tracked parts: a sending part and a receiving part, each with its own state. A unidirectional stream has only one. The sending part is the one an application drives directly:

app creates stream

first STREAM sent

RESET_STREAM

STREAM + FIN sent

RESET_STREAM

all data acked

RESET_STREAM

RESET_STREAM acked

Ready

Send

Data Sent

Data Recvd

Reset Sent

Reset Recvd

Fig. 4.1-1The sending part of a stream. It buffers in Ready, transmits (respecting flow control) in Send, finishes with a FIN into Data Sent, and reaches the terminal Data Recvd once every byte is acknowledged — or is abandoned via RESET_STREAM at any point.RFC 9000 §3.1

A stream begins in Ready (buffering, no ID committed yet). The first STREAM frame moves it to Send, where the endpoint transmits and retransmits data while respecting the peer's flow-control limits (§4.2), emitting STREAM_DATA_BLOCKED if it runs out of credit. A STREAM frame carrying the FIN bit signals the end of data and moves the part to Data Sent; once all of it is acknowledged, the part reaches the terminal Data Recvd. The receiving part mirrors this — Recv → Size Known (FIN seen) → Data Recvd → Data Read — but adds a state the sender cannot observe: whether the application has actually read the delivered bytes ([RFC 9000 §3.2]).

Three ways a stream ends #

  • Graceful finish (FIN). The normal case: the sender marks the last byte with FIN, the receiver reads to the end, and each half closes cleanly.
  • Abrupt reset (RESET_STREAM, 0x04). The sender abandons the sending part (no more data will be delivered reliably) and jumps to Reset Sent from any of Ready, Send, or Data Sent ([RFC 9000 §3.1]). Useful when the data is no longer wanted (a cancelled download).
  • Asking the peer to stop (STOP_SENDING, 0x05). The receiver of a stream tells the sender it has no use for the data; a well-behaved sender responds with RESET_STREAM ([RFC 9000 §3.5]). This is how HTTP/3 cancels a request the client no longer needs.

Because the halves are independent, one direction of a bidirectional stream can be finished or reset while the other keeps flowing.

All of this rides on one frame. The STREAM frame's type byte is itself a small bitfield: 0x08 plus an OFF bit for an explicit offset, a LEN bit for an explicit length, and the FIN bit. So "which fields does this frame have" is read straight off the type ([RFC 9000 §19.8], constants.md §4):

Note

Resetting a stream frees the application's interest in it, but not instantly its transport state: the reset itself must be acknowledged (reaching Reset Recvd), and the peer must be told the final size so connection-level flow control accounting stays correct (§4.2). "Reset" is a clean abort, not a way to make a stream vanish from the wire.

In practice

editorial The number of streams a peer will let you open is itself flow-controlled, via MAX_STREAMS and the initial_max_streams_* transport parameters (§7.2). A client that opens requests faster than the server raises the limit will stall on STREAMS_BLOCKED: a common cause of a page that loads its first N objects briskly and then pauses. If a capture shows STREAMS_BLOCKED, the fix is a higher stream limit, not more bandwidth. Design for the concurrency you expect and set the initial limits accordingly.

Takeaways #

Streams come in four types keyed by two ID bits; each has independent send and receive parts with mirrored state machines; and each ends by FIN, by RESET_STREAM, or by a STOP_SENDING that prompts one. This lifecycle is the unit that flow control and multiplexing act on. §4.2 shows how flow-control limits govern how much can be in flight on a stream and across the connection.