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:
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 withRESET_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):
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.
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.