TT Lab
Get started
Learn Learning paths Courses

Real-Time Communication — WebSocket, gRPC Streaming and WebRTC

Where head-of-line blocking survives — HTTP/3 and QUIC

Continue in TT Lab

In one line

TCP holds all the bytes behind one lost segment in order to keep order, while QUIC pushes streams down to the transport layer so that only the stream that lost something waits.

Why this was needed

Recall the numbers you saw in the last step of the previous module's lab. When we put in a relay that stops for 800ms after 20,000 bytes pass downstream, HTTP/2's small response carried on one connection finished at around 900ms, while HTTP/1.1's small response, which used separate connections, finished in tens of ms. What the relay imitated is the loss of one segment. TCP hands the byte stream to the application only in order, so while a segment in the middle waits for retransmission, the bytes that have already arrived after it are also held in the kernel buffer. HTTP/2 removed application-level head-of-line blocking, but it made every stream share the TCP-level head-of-line blocking beneath it.

Inside a data center where loss is rare, this cost is hardly visible. On mobile networks and Wi-Fi where loss is frequent, and in services such as voice where tens of ms are noticeable, the slowest 1% of requests are filled with this stall.

How it works

QUIC in RFC 9000 runs over UDP, but redoes everything TCP did — reliable delivery, congestion control (RFC 9002), and flow control. What changed is that the stream is a transport-layer concept. One packet can carry frames of several streams, but ordering is guaranteed only within a stream. If a packet is lost, only the streams that had data in that packet wait for retransmission, and the other streams are handed to the application as they arrive.

Encryption was also put inside rather than layered on the outside. RFC 9001 melts the TLS 1.3 handshake into the QUIC handshake, so that a new connection can send data at 1-RTT, and with a server it has met before, at 0-RTT. 0-RTT data is exposed to replay attacks, so use it only for idempotent requests. Because a connection is recognized by a connection ID rather than by IP and port, a phone can keep the connection going even when it moves from Wi-Fi to LTE (connection migration). It is a welcome property for sessions that last long, like voice calls.

HTTP/3 in RFC 9114 puts HTTP semantics on top of this QUIC. Header compression changed too. HPACK assumes header blocks arrive in the order sent and updates its dynamic table accordingly, so it cannot be used in QUIC, where streams arrive separately. So QPACK in RFC 9204 sends table updates on separate encoder and decoder streams, and agrees through a setting (SETTINGS_QPACK_BLOCKED_STREAMS) on how many request streams may wait because they point at table entries that have not yet arrived.

Layer HTTP/1.1 HTTP/2 HTTP/3
Request concurrency Several connections Streams on one connection QUIC streams on one connection
Effect of one loss Only that connection All streams Only that stream
Header compression None HPACK QPACK
Encryption Optional (TLS) Effectively TLS Always (TLS 1.3 built in)

Whether HTTP/3 is available is announced by the server through the Alt-Svc header or an HTTPS record in DNS, and the client sees that and tries UDP from the next request on.

Flow control stays two-layered too. QUIC announces the maximum number of bytes it can receive per stream and for the whole connection (MAX_STREAM_DATA and MAX_DATA frames), and separately announces the number of streams that can be open at once with MAX_STREAMS. What SETTINGS and WINDOW_UPDATE did in HTTP/2 has moved down to the transport layer. So the phenomenon from the earlier lab, "when the connection window closes, even a small response waits," happens in HTTP/3 in exactly the same way. What QUIC removed is the blocking caused by loss, not the blocking caused by a slow receiver.

What it looks like in the field

HTTP/3 does not always win. There are still corporate networks and public Wi-Fi that block UDP 443, so the client has to be ready to fall back to TCP, and since it does encryption and congestion control in user space, it uses more CPU for the same throughput. If load balancers and firewalls do not understand QUIC, the benefit of connection migration disappears as well. So large services keep both paths open and choose by metrics.

The lab Pods in this course do not have the NET_ADMIN privilege, so they cannot inject real packet loss with tc netem. So on the TCP side, a user-space relay imitated the result of loss (the stall), and QUIC is covered by this reading. Writing down what you imitated and what you did not imitate when simulating loss is also part of measurement — the relay does not reproduce the retransmission timer or the shrinking of the congestion window.

What you will do in the next quiz

You tell apart TCP-level and application-level head-of-line blocking, and check which one QUIC removed and how, and why QPACK was needed.