Learning objective. Follow an HTTP/3 request and response across their bidirectional stream, including interim responses, and see how many such exchanges run concurrently and interleave over one connection.
One exchange, one bidirectional stream #
A request/response exchange occupies a single client-initiated bidirectional stream (§1.3). The two halves of that stream carry the two directions (§4.1):
- The client sends its request as a HEADERS frame, then zero or more DATA frames for a body, then optionally trailing HEADERS, and marks the send side done with the QUIC FIN bit. A client sends exactly one request per stream ([RFC 9114 §4.1]).
- The server replies on the same stream: zero or more interim responses (1xx, such as
103 Early Hints), then exactly one final response (its HEADERS, DATA, and optional trailers), ending with FIN.
Interim responses are the reason the exchange is more than request-then-response: a server
can send 103 Early Hints to let the client start fetching sub-resources before the final
response is ready, all on the same stream before the 200 arrives.
Concurrency is just more streams #
There is no separate multiplexing layer to configure: concurrency is opening more streams. A browser loading a page opens a bidirectional stream per request — 0, 4, 8, … — and they proceed in parallel. Three chapter-4 mechanisms govern how that parallelism behaves:
- Isolation. A loss on one request's stream does not stall the others (§4.4).
- Ordering. When the streams compete for the connection's send capacity, the server interleaves their response data by priority (§4.3), which is what lets a small critical response finish ahead of a large image already in flight.
- Limits. The number of concurrent requests is capped by the peer's stream limit (§4.1) and their total in-flight data by connection flow control (§4.2).
Because the request half closes (FIN) as soon as the request is sent while the response half keeps flowing, a stream spends most of its life half-closed, the client done sending, the server still responding. That is the normal steady state, not an error.
Server push, briefly #
HTTP/3 keeps server push: the server sends a PUSH_PROMISE on the request stream to
promise a response the client did not ask for, then delivers it on a server-initiated
push stream, bounded by the client's MAX_PUSH_ID ([RFC 9114 §4.6]). In practice push
is little used: the savings rarely justify the complexity and wasted bandwidth of pushing
resources a client may already have, and 103 Early Hints has become the preferred way to
accelerate sub-resource loading. Push is worth recognizing in a trace; it is rarely worth
enabling.
"Concurrent" does not mean "simultaneous on the wire." At any instant the connection is sending bytes for whichever stream the scheduler picked (§4.3); concurrency means many streams are open and making progress over time, interleaved through the one send path. The illusion of parallelism comes from fine-grained interleaving, not from parallel transmission.
editorial The most common concurrency surprise is a page
that opens far more requests than the server's initial stream limit, then stalls on
STREAMS_BLOCKED (§4.1) partway through loading: visible as a burst of requests,
a pause, then another burst as the limit is raised. Provision initial_max_streams_bidi for
the fan-out you expect. And prefer 103 Early Hints over server push for getting critical
sub-resources to the client early; it achieves the latency win without guessing at the client's
cache.
Takeaways #
An HTTP/3 exchange is one bidirectional stream carrying a request and, after any interim responses, a single final response; concurrency is simply many such streams, isolated by the transport, interleaved by priority, and bounded by stream and flow-control limits. When any of that goes wrong, the failure surfaces as a stream reset or a connection error, the subject of §5.4.