Learning objective. Explain precisely why QUIC's streams remove the transport head-of-line blocking that afflicts HTTP/2 over TCP, what "per-stream" ordering still guarantees, and the one place blocking sneaks back in.
The same loss, two outcomes #
§1.1 introduced the problem; here is the mechanism. TCP exposes a single, strictly ordered byte stream. HTTP/2 multiplexes many logical streams into that one byte stream, so when a segment is lost, the receiver's TCP stack must hold back every later byte (including fully-arrived bytes for unrelated streams) until the gap is retransmitted. The multiplexing is real, but it rides on a transport that cannot deliver out of order.
QUIC gives each stream its own ordered byte space and its own reassembly. The receiver can hand stream B's bytes to the application while stream A still has a hole, because nothing about A's missing data constrains B's offsets ([RFC 9000 §2]). One lost packet delays only the stream(s) whose data it carried.
What "per-stream" actually promises #
The guarantee is precise, and the precision matters. QUIC removes head-of-line blocking between streams; it does not remove ordering within a stream. A byte lost early in stream A still blocks later bytes of stream A — that is the reliability and ordering a stream promises (§4.1). So the correct statement is: a loss is confined to the stream it hit. If you multiplex independent objects onto separate streams, a loss affecting one leaves the others untouched; if you serialize independent objects onto a single stream, you have rebuilt TCP's problem inside one QUIC stream.
This is the actionable design rule of the whole chapter: give independent things independent streams. An HTTP/3 connection does this automatically: one bidirectional stream per request (§1.3). That is exactly why a lost packet for one response does not stall the others.
Where blocking sneaks back #
"No head-of-line blocking" is true only when scoped carefully. Two residual couplings remain, both covered later:
- QPACK header decompression. If a HEADERS block references dynamic-table entries that
arrived on the (separate) QPACK encoder stream but have not yet been processed, the decoder
must wait, reintroducing a cross-stream stall at the compression layer, not the transport
(§6.3). QUIC bounds this with
SETTINGS_QPACK_BLOCKED_STREAMS. - Shared connection flow control. A stream whose application has stopped reading holds
buffered bytes against the connection-level
MAX_DATAwindow, starving other streams of credit (§4.2): a flow-control stall rather than a delivery stall.
Neither is the transport-level, one-loss-stalls-all blocking of TCP; both are narrower and independently mitigated. Naming which layer a stall lives in is most of diagnosing it.
Datagrams (§3.4) sidestep the question entirely: DATAGRAM frames are not
ordered or retransmitted, so there is no head-of-line to block. That is the right tool when
even per-stream ordering is more than the application wants — but it trades away reliability
to get there.
editorial The migration from HTTP/2 to HTTP/3 quietly changes what "bundle your assets" means. Under HTTP/2, concatenating many small files into one big response reduced per-stream overhead and dodged some prioritization pitfalls; under HTTP/3 it can hurt, because a single loss in that big response now blocks all of it, where separate streams would have isolated the damage. On a lossy path, prefer more, smaller streams over fewer, larger ones: the opposite of the old bundling instinct (§4.5).
Takeaways #
QUIC confines a loss to the stream it hit by giving every stream its own ordered byte space; per-stream ordering still holds, so the rule is to map independent work onto independent streams. The only residual blocking lives above the transport (QPACK decompression and shared flow control), and each is separately bounded. §4.5 puts streams, flow control, priority, and this isolation together into a concrete stream plan for a mixed workload.