Framing versus compression — two headers people conflate
Transfer-Encoding and Content-Encoding look similar but mean different
things. One frames the message for a single connection hop (chunked); the other
compresses the representation end-to-end (gzip, br). Confusing them leads to
double-compression bugs and caches serving undecodable bodies. This reference
sorts every coding value into the right layer.
How it works
Content-Encodingis end-to-end. The body is compressed (gzip,br,deflate,zstd) and a cache stores it that way; the client decodes it. It is negotiated viaAccept-Encodingand recorded inVary.Transfer-Encodingis hop-by-hop, applied for one connection then stripped by the next. Its dominant value ischunked, which streams a body of unknown length as size-prefixed chunks ending in a zero-length chunk:
Transfer-Encoding: chunked
7\r\n
Mozilla\r\n
0\r\n
\r\n
Multiple codings are applied left-to-right and decoded right-to-left, with
chunked always last in a Transfer-Encoding list. In HTTP/2 and HTTP/3 the
binary framing replaces all of this, so Transfer-Encoding is prohibited there.
Coding values quick reference
| Coding | Header | Type | Notes |
|---|---|---|---|
chunked | Transfer-Encoding | Framing | Only used in HTTP/1.1; forbidden in HTTP/2+ |
gzip | Content-Encoding | Compression | Most widely supported; uses DEFLATE with gzip wrapper |
br | Content-Encoding | Compression | Brotli; typically 15-25% smaller than gzip |
deflate | Content-Encoding | Compression | Ambiguous in practice (zlib vs raw DEFLATE); avoid |
zstd | Content-Encoding | Compression | Zstandard; fast decompression, growing browser support |
identity | Content-Encoding | No-op | Explicit “no encoding applied” signal |
Why HTTP/2 removed chunked
Chunked transfer-encoding exists to solve an HTTP/1.1 problem: the server did not
know the response body size in advance, so it could not set Content-Length.
Chunked framing let the server stream data of unknown length by prefixing each
chunk with its size.
HTTP/2 solved this differently. Its binary framing layer sends data as typed
frames, and the connection’s own framing mechanism handles body length without
needing a special transfer-coding. The Transfer-Encoding: chunked header is
therefore prohibited in HTTP/2 — sending it is a protocol error that causes
browsers to close the connection or trigger an error response.
This is a common source of bugs when migrating from HTTP/1.1 to HTTP/2 or when
a reverse proxy rewrites the connection. The proxy terminates the HTTP/1.1
chunked connection from the backend but must not pass the Transfer-Encoding
header on to the HTTP/2 client.
Choosing between gzip, br, and zstd
Modern browsers support all three compression codings, but the right choice depends on your deployment:
gzipis the safest choice for broad compatibility. All HTTP clients and CDNs support it. Use it for any public-facing API or when CDN support matters.br(Brotli) typically achieves better compression ratios than gzip for text content (HTML, CSS, JSON) but is slower to compress at high compression levels. It is ideal for static assets served from a CDN that pre-compresses.zstdhas very fast decompression and good compression ratios. It has growing browser support and is well-suited to high-throughput APIs. It is also the preferred codec for many server-to-server streaming APIs where both ends are known to support it.
For dynamic responses, gzip is still the standard. For static assets, br at a high compression level (pre-computed offline) reduces transfer size most effectively. For internal service-to-service communication, zstd is worth benchmarking.
Tips and notes
- Compress with
Content-Encoding, notTransfer-Encoding, in practice. - Never set both a
Content-LengthandTransfer-Encoding: chunked— they are mutually exclusive in HTTP/1.1. - Do not double-compress: a body already compressed with
brserved withgzipContent-Encodingis corrupt and undecodable by the client. - Trailers ride after the final chunk; advertise them with the
TE: trailersrequest header. - Set
Vary: Accept-Encodingon compressed responses so caches serve the correct encoding to clients that do not support compression.