skip to content

On an HTTP/2 connection carrying ten concurrent requests, one TCP segment is lost. What happens to the other nine responses, and how does QUIC's per-stream loss recovery change the outcome for HTTP/3?

level: middleimportance: must knowfreq 48%

answer

  1. TCP = one ordered byte stream → gap blocks everything
  2. kernel buffers the bytes; app sees nothing for ~1 RTT
  3. QUIC: STREAM frames with per-stream offsets and buffers
  4. retransmit in a NEW packet number (no ambiguity)
  5. congestion control still connection-wide; QPACK can still block

basics

~20 s

TCP delivers one ordered byte stream, so the kernel holds back every byte after the lost segment — all nine other responses stall for a retransmission round trip even though their data arrived. QUIC tracks loss per packet and delivers each stream independently, so only streams with data in the lost packet wait.

solid answer

~60 s

HTTP/2 multiplexes at the HTTP layer but rides a single TCP byte stream. TCP guarantees in-order delivery of that stream, so when a segment is lost the receiving kernel buffers everything that arrived after it and hands the application nothing until the retransmission lands — roughly one RTT later. Frames belonging to the other nine responses are sitting in the receive buffer, complete and unreadable. That is **transport-level head-of-line blocking**, and HTTP/2's multiplexing cannot fix it because the ordering constraint is a layer below. QUIC removes the constraint by making streams first-class in the transport. Each packet carries frames tagged with stream IDs and offsets; the receiver reassembles per stream. A lost packet blocks only the streams whose bytes it carried, so the other responses are delivered immediately, and the lost data is retransmitted as new frames in a new packet (packet numbers never repeat, which also removes TCP's retransmission ambiguity). The practical effect grows with loss rate and with the number of concurrent streams: negligible on a clean link, large on lossy mobile networks.

code

bash · 4 lines
bash
sudo tc qdisc add dev eth0 root netem loss 2% delay 100ms
curl -o /dev/null -s -w 'h2 total=%{time_total}\n' --http2 https://example.com/page
curl -o /dev/null -s -w 'h3 total=%{time_total}\n' --http3-only https://example.com/page
sudo tc qdisc del dev eth0 root

go deeper

for a junior

Know that TCP delivers bytes strictly in order, so one lost segment stalls every multiplexed response until it is retransmitted, and QUIC does not have that coupling.

for a middle

Explain the kernel-buffer mechanism, name STREAM frames with per-stream offsets, and state that only affected streams wait under QUIC.

for a senior

Quantify when it matters (loss rate times concurrency, tail latency rather than median) and name the residual couplings: congestion control and QPACK.

for a principal

Discuss the concentration tradeoff — one connection is better on clean paths and worse under loss — and how you would measure it credibly with shaped links and percentile distributions before committing to HTTP/3.

## Two different head-of-line problems It helps to separate them explicitly, because interviewers probe this. **Application-layer HOL (HTTP/1.1).** One connection carries one exchange at a time, so a slow response blocks the requests queued behind it. HTTP/2 solved this with streams. **Transport-layer HOL (TCP).** TCP presents a single ordered byte stream. The receiving kernel must deliver bytes in order, so a gap caused by a lost segment stops *all* subsequent bytes from reaching the application, regardless of which HTTP stream they belong to. HTTP/2 did not solve this, and could not: it has no visibility below the socket. ## Walking the HTTP/2 case Ten responses are interleaving. The server writes a burst of frames; the network drops one segment in the middle. 1. Later segments arrive and sit in the kernel receive buffer. 2. The kernel cannot deliver them because there is a gap in the sequence space. 3. The client's HTTP/2 stack reads nothing — not even the frames for streams unaffected by the loss. 4. The receiver sends duplicate ACKs or SACK; the sender retransmits; roughly one RTT later the gap is filled. 5. Now the kernel delivers the whole buffered run at once, and the HTTP/2 stack parses ten responses' worth of frames in a burst. Everything stalls together. Worse, the effect scales with how much you multiplexed: with six HTTP/1.1 connections a loss stalls one sixth of the traffic; with one HTTP/2 connection it stalls all of it. This is the well-known result that HTTP/2 can underperform HTTP/1.1 on high-loss links. ## What QUIC does differently QUIC moves stream awareness into the transport: - A QUIC packet contains **frames**. STREAM frames carry a stream ID, a byte offset, and data. - The receiver keeps a separate reassembly buffer per stream. - When a packet is lost, only the streams that had STREAM frames in that packet have gaps. Every other stream is complete up to its latest offset and is delivered to the application immediately. So in the same scenario, if the lost packet held data for stream 3 only, streams 1, 5, 7, … 19 are handed to the HTTP/3 layer without waiting, and only stream 3's consumer waits a retransmission. Two supporting mechanics are worth naming: - **Monotonic packet numbers.** A retransmission is sent in a *new* packet with a *new* number, carrying the same stream data at the same offsets. TCP reuses sequence numbers on retransmission, creating ambiguity about which transmission an ACK refers to; QUIC's design removes it, so RTT estimation and loss detection are more precise. - **Richer acknowledgements.** QUIC ACK frames carry many ranges by default (more than typical SACK), improving recovery under heavy loss. ## What is *not* removed - **Per-stream ordering still exists.** Within one response, bytes are delivered in order, so a loss does delay that response. - **Congestion control is still connection-wide.** Loss signals reduce the sending rate for the whole connection, so a lossy path still slows everything down — QUIC removes the *delivery* coupling, not the *rate* coupling. - **HTTP/3 header compression can reintroduce a stall.** QPACK's dynamic table creates cross-stream dependencies; a header block referencing a table entry that arrived in a lost packet must wait. QPACK is designed with a blocked-streams limit to bound this, and encoders can avoid dynamic references entirely at a compression cost. - **Application-level ordering** you impose yourself (for example, a client that will not render until asset A arrives) is unaffected. ## When it actually matters The benefit is proportional to loss and concurrency. On a fibre link with 0.01% loss, transport HOL is a rounding error. On a congested mobile network at 1–3% loss with dozens of concurrent objects, it is the dominant tail-latency effect, and this is where HTTP/3 measurements show real gains — usually in the 95th/99th percentile rather than the median. ## Demonstrating it To show the difference credibly you need a controlled loss environment: shape a link with `tc netem` (for example 2% loss, 100 ms RTT), load the same page over HTTP/2 and HTTP/3, and compare the distribution of object completion times, not the average. Look at qlog traces on the QUIC side to see which streams were actually blocked by which lost packets.

  • Does HTTP/3 eliminate head-of-line blocking completely?
    No. Ordering within a single stream still applies, so a loss delays that response. Congestion control remains connection-wide, so loss still reduces the sending rate for everything. And QPACK's dynamic table can make a header block on one stream depend on data that was lost on another, which is why QUIC limits how many streams may be blocked that way.
  • Why can HTTP/2 be slower than HTTP/1.1 on a lossy network?
    HTTP/1.1 spreads traffic over about six TCP connections, so a loss on one stalls only the requests on that connection while the others keep delivering. HTTP/2 concentrates everything on a single connection, so one lost segment stalls delivery of every concurrent response for a retransmission round trip. Concentration is a win on clean links and a liability under loss.

TCP is a single conveyor belt: one jammed box stops every parcel behind it, even those for other addresses. QUIC gives each address its own belt inside the same truck, so a jam only delays that one destination.

saying these in an interview costs you the question

  • Claiming HTTP/2 multiplexing already removes head-of-line blocking at every layer
  • Saying QUIC removes head-of-line blocking entirely, ignoring per-stream ordering, QPACK, and shared congestion control
  • Believing the stalled bytes were never received — they arrive and sit unreadable in the kernel buffer
  • Expecting a large HTTP/3 median improvement on a clean, low-loss link

context