HTTP/2: Multiplexing and Header Compression
What HTTP/2 changed over HTTP/1.1: many interleaved streams on one connection and HPACK header compression. A simulation and the Python hpack library show the gains, and where TCP still limits it.
HTTP/2 multiplexing sends many requests and responses at once as interleaved binary frames of separate streams on one TCP connection, and HPACK compresses repeated headers into small table indexes. Interviewers expect the limit: HTTP/2 removes head-of-line blocking at the HTTP layer, but one lost TCP packet still stalls every stream, which HTTP/3 over QUIC addresses.
HTTP/2 sends many requests at once as interleaved streams on a single TCP connection, and compresses repeated headers with HPACK. In a test, a request's headers shrank from 238 bytes to 133 on first send and 18 on repeats. It removes HTTP-level blocking but not TCP-level blocking, which HTTP/3 addresses.
- In HTTP/2, every request and response pair is a stream with a numeric id, and each message is cut into binary frames such as HEADERS and DATA that carry that id.
- With the Python hpack library, a request of 238 bytes as HTTP/1.1 text encoded to 133 bytes on first send and 18 bytes for each later request that changed only the path.
- In a simulation of one 6-unit response and two 1-unit responses, serial HTTP/1.1 finished the small ones at 7 and 8, while HTTP/2 interleaving finished them at 2 and 3.
- HTTP/2 removes head-of-line blocking at the HTTP layer but not at the TCP layer, because one lost packet delays every stream sharing the connection.
- HTTP/3 runs over QUIC on UDP, where each stream is delivered independently, so a lost packet only stalls its own stream.
- A client learns that a server supports HTTP/2 through ALPN during the TLS handshake, and browsers use HTTP/2 only over TLS in practice.
01The problem with HTTP/1.1
In HTTP/1.1 a connection carries one request and response at a time. If a big response is first, everything behind it waits. This is head-of-line blocking at the HTTP level. Browsers worked around it by opening up to six connections per host, and sites split resources across several domains. Each extra connection costs a TCP handshake, a TLS handshake and slow-start ramp-up.
Headers are also sent as plain text on every request. A typical request repeats the same user-agent, accept and cookie headers hundreds of bytes long, again and again, even though they have not changed.
02Streams and frames
HTTP/2 keeps the same meaning (methods, status codes, headers) but changes how bytes travel. Every request and response pair is a stream with a numeric id. Each message is cut into small binary frames (HEADERS, DATA and a few others), and each frame carries its stream id. Frames from different streams can be mixed on one connection, and the receiver reassembles each stream by id.
That is multiplexing: one TCP connection, many requests in flight at once, no need for six parallel connections. The binary framing is also simpler to parse than text and less error-prone.
03A tiny simulation of the effect
Suppose one response needs 6 time units to send and two others need 1 each, all requested together, big one first.
sizes = {'big': 6, 'a': 1, 'b': 1}
# HTTP/1.1 style: one after another
t, fin = 0, {}
for n in ['big', 'a', 'b']:
t += sizes[n]; fin[n] = t
print(fin) # {'big': 6, 'a': 7, 'b': 8}
# HTTP/2 style: one frame from each stream in turn
rem, t, fin2 = dict(sizes), 0, {}
while rem:
for n in list(rem):
t += 1; rem[n] -= 1
if rem[n] == 0: fin2[n] = t; del rem[n]
print(fin2) # {'a': 2, 'b': 3, 'big': 8}Serially, the small responses finish at 7 and 8 because they sit behind the big one. Interleaved, they finish at 2 and 3. The big response finishes at 8 instead of 6, and the total time is the same. Multiplexing does not make the link faster. It makes small, important responses stop waiting behind large ones, which is what a page load wants. This is a simplified model with equal frame sizes and no priorities.
04Header compression with HPACK
HTTP/2 compresses headers with HPACK. Both sides keep a dynamic table of headers seen so far. A header already in the table is sent as a small index instead of its full text. There is also a static table of common headers (:method GET, accept-encoding, and so on) and Huffman coding for strings. HPACK was designed specifically to avoid compression attacks such as CRIME that hit general-purpose compression on headers.
With the hpack Python library I encoded a request with eight headers including a long user-agent and a cookie, then three more requests where only the path changed:
from hpack import Encoder
base = [(':method', 'GET'), (':scheme', 'https'), (':authority', 'example.com'),
(':path', '/api/items'),
('user-agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/126 Safari/537.36'),
('accept', 'application/json'), ('accept-encoding', 'gzip, br'),
('cookie', 'session=abc123def456ghi789; theme=dark')]
enc = Encoder()
sizes = []
for i in range(4):
h = list(base)
h[3] = (':path', f'/api/items/{i}')
sizes.append(len(enc.encode(h)))
print(sizes) # [133, 18, 18, 18]The same request as HTTP/1.1 text (request line, Host and headers) is 238 bytes. HPACK sent 133 bytes the first time (a little over half) and 18 bytes for each later request, because everything unchanged was already in the table. Over a page with dozens of requests, and with large cookies, the saving is substantial.
05What HTTP/2 does not fix
All HTTP/2 streams share one TCP connection, and TCP guarantees in-order delivery. If one packet is lost, TCP holds back every later byte, for every stream, until the lost packet is retransmitted. So HTTP/2 removed head-of-line blocking at the HTTP layer but left it at the transport layer. On a lossy network, a single connection can do worse than six parallel HTTP/1.1 ones.
HTTP/3 addresses this by running over QUIC, built on UDP, where each stream is delivered independently and a loss only stalls its own stream. QUIC also folds the TLS handshake into connection setup.
06Practical notes
Some HTTP/1.1 optimisations become unnecessary or harmful: sharding assets across many domains (it prevents reuse of one connection), and bundling every script into one giant file (it hurts caching, since one change invalidates everything). Smaller, cacheable files are fine under HTTP/2.
Server push, which lets a server send resources before they are requested, turned out hard to use well, and Chrome removed support for it in 2022, so it is rarely used. Flow control lets a receiver avoid being overwhelmed. The original HTTP/2 stream-priority scheme was deprecated in RFC 9113 and replaced by the simpler Extensible Priorities of RFC 9218. Browsers use HTTP/2 only over TLS in practice, and the protocol is negotiated during the TLS handshake (ALPN).
07Common interview questions
Q: What is multiplexing in HTTP/2?
A: Sending many requests and responses at once as interleaved frames of separate streams on one connection.
Q: How does HTTP/2 reduce header overhead?
A: HPACK uses static and dynamic tables and Huffman coding, so repeated headers are sent as small indexes.
Q: Does HTTP/2 remove head-of-line blocking?
A: At the HTTP layer yes. At the TCP layer no, because one lost packet delays all streams on the connection.
Q: How does HTTP/3 differ?
A: It uses QUIC over UDP, so streams are independent and a lost packet only blocks its own stream.
Q: Is HTTP/2 text or binary?
A: Binary framing, with the same HTTP semantics as before.
Q: How does a client learn the server supports HTTP/2?
A: Through ALPN during the TLS handshake.