skip to content

HTTP/1.1 sends request headers as plain ASCII text on every single request. What changed in HTTP/2, and why was compressing header fields considered worth the extra complexity?

level: juniorimportance: should knowfreq 40%

answer

  1. headers were never compressed in h1.1
  2. ~700 bytes repeated per request, cookies worst
  3. binary framing + HPACK, pseudo-headers :method/:path
  4. win scales with request count, not bytes
  5. per-connection table state on the server

basics

~20 s

HTTP/2 encodes headers in binary and compresses them with HPACK. Real requests repeat almost identical headers (cookies, user-agent, accept) worth hundreds of bytes each. On pages making many small requests that overhead dominated; HPACK turns repeats into tiny index references.

solid answer

~50 s

In HTTP/1.1 every request re-sends its whole header block as literal text. A typical browser request is 500-800 bytes of headers, and nearly all of it is byte-identical to the previous request on the same connection: same `User-Agent`, same `Accept-*`, same `Cookie`. Bodies had `Content-Encoding: gzip` for decades; headers were never compressed. That was tolerable when a page made ten requests. It stopped being tolerable when pages made a hundred requests for small assets, and it hurt most on the uplink, which is the narrow direction on mobile: a few kilobytes of duplicated request headers can cost extra round trips before the requests are even out. HTTP/2 replaced text framing with binary HEADERS frames and added **HPACK**: a fixed static table of common header names and values, a per-connection dynamic table of fields already sent, and Huffman coding for literal strings. A repeated request often collapses to a few dozen bytes on the wire.

code

http · 8 lines
http
GET /assets/icon.png HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36
Accept: image/avif,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Referer: https://example.com/dashboard
Cookie: session=8f3a1c...; _ga=GA1.2.1837...; ab_bucket=17

go deeper

for a junior

Know that HTTP/1.1 repeats plaintext headers on every request, that HTTP/2 is binary and compresses them with HPACK, and that cookies are the usual bulk.

for a middle

Be able to name the three HPACK mechanisms (static table, dynamic table, Huffman) and explain that the win scales with request count on the uplink.

for a senior

Connect it to real page-load behaviour: many small requests, small initial congestion window, round trips saved; and note the per-connection memory cost on intermediaries.

for a principal

Frame it as a fixed per-request overhead reduction and be explicit about where it does not pay - low-request-count APIs, large transfers - and about the state cost at the edge tier.

## What a header field costs A header field is a `name: value` pair sent ahead of the body, for example `Host: example.com` or `Cookie: session=8f3a...`. In HTTP/1.1 these travel as literal ASCII lines, once per request and once per response, with no compression at all. Two properties make that expensive at scale. **They are large.** A modern browser request carries `User-Agent` (often 100+ bytes), `Accept`, `Accept-Encoding`, `Accept-Language`, `Referer`, several `Sec-Fetch-*` fields, and cookies. Cookies are the worst offender: a site that sets a few analytics and session cookies can push 1-2 KB onto *every* request to that origin, including the request for a 300-byte icon. **They are nearly identical.** Between two requests on the same connection to the same origin, typically only the path changes. Everything else repeats verbatim. Sending the same 700 bytes 100 times is 70 KB of pure redundancy. ## Why HTTP/1.1 could not just gzip them Nothing in HTTP/1.1 defines header compression. `Content-Encoding` and `Transfer-Encoding` apply to the message body, and intermediaries — proxies, caches, load balancers — must be able to read and rewrite headers, so retrofitting a compressed header block into the existing text protocol was not feasible. HTTP/2 could do it because it redefined framing entirely: a HEADERS frame carries an opaque compressed header block fragment, and every endpoint on the connection is required to understand the new encoding. ## What HTTP/2 actually does Two separate changes, often conflated: 1. **Binary framing.** Header fields become a compact binary representation rather than CRLF-delimited text, and the request line and status line become pseudo-header fields (`:method`, `:scheme`, `:authority`, `:path`, `:status`). Parsing is length-prefixed instead of delimiter-scanning, which also removes a whole class of request-smuggling ambiguity. 2. **HPACK compression** (RFC 7541). Three mechanisms combine: a **static table** of 61 predefined entries covering the commonest name/value pairs, a **dynamic table** built up per connection as fields are sent, and **Huffman coding** of any string that still has to be sent literally. The result is that the first request on a connection pays roughly its literal (Huffman-coded) size, and subsequent requests pay a byte or two per unchanged field. Measured on real page loads, request header bytes typically drop by 80-90%. ## Why the win is uplink-shaped Downlink bandwidth is usually plentiful; uplink is not, especially on cellular. A browser opening a page fires many requests near-simultaneously. Under HTTP/1.1 that is N times 700 bytes upstream, which on a slow uplink with a small initial congestion window can take multiple round trips just to transmit the *requests*. Compressing them can remove a full round trip from time-to-first-byte on a resource-heavy page. This is exactly the regime HTTP/2 was designed for: many small requests to one origin. ## What it does not help If your traffic is a handful of large requests — a video download, a big file upload, one JSON API call per screen — header bytes are noise and HPACK saves you nothing measurable. Header compression is a per-request fixed-cost reduction, so its value scales with request count, not with bytes transferred. ## Where it shows up operationally Because the dynamic table is per-connection state on both peers, HPACK is not free for servers: an intermediary holding a million concurrent HTTP/2 connections holds a million pairs of tables. That is why the table size is negotiable and why proxies advertise conservative limits. HTTP/3 keeps the idea but replaces HPACK with QPACK, because HPACK's table updates assume a single strictly ordered byte stream and QUIC does not provide one across streams.

  • Does HTTP/2 header compression apply to responses as well as requests?
    Yes. HPACK is symmetric: each direction has its own encoder and its own dynamic table, so response headers such as repeated Content-Type, Server, Cache-Control and Set-Cookie compress the same way. In practice the request-side win is larger because request headers repeat more and the uplink is narrower.
  • If headers are compressed, is it still worth trimming cookies and custom headers?
    Yes. The first occurrence still costs its full Huffman-coded size, large values can evict other entries from the dynamic table, and any value that changes per request - a request id, a signed token, a rotating cookie - never benefits from indexing at all. Compression reduces repetition, it does not make bloated headers free.

Like a courier who, instead of rewriting the full sender address on every envelope, writes it once and afterwards just writes 'sender #3', referring to a shared numbered list both parties keep.

saying these in an interview costs you the question

  • Saying HTTP/1.1 compressed headers with gzip via Content-Encoding - that only ever applied to the body
  • Claiming HPACK is just gzip applied to the header block
  • Thinking header compression speeds up large file transfers
  • Assuming compression is free for the server and ignoring per-connection table memory

context