skip to content

What is the difference between the HTTP Content-Encoding and Transfer-Encoding headers?

level: seniorimportance: should knowfreq 34%

answer

  1. representation vs connection
  2. end-to-end vs hop-by-hop
  3. chunked = framing, not compression
  4. CL + TE together ⇒ smuggling, must reject
  5. no Transfer-Encoding in HTTP/2 or /3

basics

~20 s

Content-Encoding is end-to-end: it transforms the representation itself, so only the final recipient decodes it and caches store it encoded. Transfer-Encoding is hop-by-hop framing for one connection — chunked is its only real use — and it does not exist in HTTP/2 or HTTP/3.

solid answer

~50 s

**Content-Encoding** is a property of the *representation*. `Content-Encoding: gzip` means the resource's bytes have been transformed; the transformation survives caching and proxying end to end, `ETag` identifies the encoded octets, and only the final recipient decodes it. It is negotiated with `Accept-Encoding`. **Transfer-Encoding** is a property of the *message on one connection*. It is hop-by-hop: each intermediary may strip and reapply it. Its real-world use is `Transfer-Encoding: chunked`, which frames a body of unknown length so the connection can stay alive without `Content-Length`. It is negotiated with `TE`, and compression as a transfer coding (`Transfer-Encoding: gzip`) is essentially unimplemented. Two consequences worth stating. First, `Content-Length` and `Transfer-Encoding: chunked` are mutually exclusive — accepting both is the root of HTTP request smuggling, so RFC 9112 requires rejecting such messages. Second, `Transfer-Encoding` **does not exist in HTTP/2 or HTTP/3**: framing is handled by the binary layer, while `Content-Encoding` carries over unchanged.

code

http · 5 lines
http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip
Transfer-Encoding: chunked
Vary: Accept-Encoding

go deeper

for a junior

Give the one-liner: Content-Encoding compresses the content end to end; Transfer-Encoding (chunked) is about how the message is framed on the connection.

for a middle

Add that chunked exists for unknown-length bodies, that the two headers can coexist, and that Content-Length and chunked cannot.

for a senior

Discuss hop-by-hop versus end-to-end consequences for caches and ETags, the CL/TE request-smuggling class, and the fact that HTTP/2 and /3 removed Transfer-Encoding entirely.

for a principal

Explain why compression-as-transfer-coding was architecturally cleaner yet lost to intermediary reality, and what that implies for normalising framing at the edge across protocol versions.

## Two different layers HTTP separates *what the resource is* from *how this particular message is put on this particular connection*. - **Content-Encoding** answers: has the representation been transformed, and how do I get the payload back? It belongs to the resource. - **Transfer-Encoding** answers: how are the bytes of this message framed on this hop? It belongs to the connection. Everything else follows from that split. ## End-to-end versus hop-by-hop `Content-Encoding` is **end-to-end**. A gzipped response passes through proxies and caches still gzipped; the cache stores the compressed octets; only the user agent decodes. That is why `Vary: Accept-Encoding` matters — the stored entity really is a different byte sequence per coding — and why `ETag` must differ between the gzip and Brotli representations of the same resource. Get that wrong and a conditional request can splice a gzip validator onto a Brotli body. `Transfer-Encoding` is **hop-by-hop**. A proxy is entitled to receive a chunked message and forward it with `Content-Length`, or the reverse. It is never cached, never part of the entity, and never seen by application code that reads a parsed body. ## Chunked: the one transfer coding that matters ``` HTTP/1.1 200 OK Content-Type: application/json Transfer-Encoding: chunked 1a\r\n{"status":"processing"}\r\n\r\n 0\r\n\r\n ``` Chunked framing lets a server start sending before it knows the total length — streaming, server-generated pages, long-running exports — while keeping the connection reusable. Before HTTP/1.1, the only way to signal "body ends here" without a length was to close the connection. Chunked composes with `Content-Encoding`: a streamed, gzipped JSON response carries `Content-Encoding: gzip` **and** `Transfer-Encoding: chunked`. They are not alternatives; they answer different questions. ## Why compression as a transfer coding never happened On paper, `Transfer-Encoding: gzip` with `TE: gzip` is the architecturally cleaner way to compress *for transport* — it is per-hop, so a cache stores the entity once in identity form and each hop compresses as appropriate, and no `Vary` gymnastics are needed. In reality almost nothing implements it: intermediaries mishandled `Transfer-Encoding` values other than `chunked` badly enough that the ecosystem standardised on `Content-Encoding` for compression. Knowing *why* the theoretically nicer option lost is the senior-level part of this answer. ## Content-Length interactions and request smuggling A message must not carry both `Content-Length` and `Transfer-Encoding: chunked`. If it does, different implementations disagree about which wins — a front-end proxy may honour one and the back-end the other, letting an attacker hide a second request inside the body. That is **HTTP request smuggling** (CL.TE / TE.CL). RFC 9112 therefore requires that such a message be rejected, and hardened front-ends normalise or refuse messages with conflicting framing. When `Content-Encoding` is present and `Content-Length` is used, the length is that of the **encoded** bytes — the octets actually on the wire. ## HTTP/2 and HTTP/3 HTTP/2 and HTTP/3 abolish `Transfer-Encoding` entirely. Framing is a job of the binary protocol: DATA frames carry the body and the stream ends when END_STREAM is set, so there is nothing for a chunked coding to do. Sending `Transfer-Encoding` in HTTP/2 is a protocol error, and `Connection`-style hop-by-hop headers are likewise banned. Bodies of unknown length are simply streams that end. `Content-Encoding`, being about the representation rather than the connection, is unaffected and works identically in HTTP/1.1, /2 and /3. ## A quick diagnostic table | | Content-Encoding | Transfer-Encoding | |---|---|---| | Scope | end-to-end (representation) | hop-by-hop (connection) | | Negotiated by | `Accept-Encoding` | `TE` | | Typical value | `gzip`, `br`, `zstd` | `chunked` | | Cached as-is | yes | no, stripped | | Affects ETag | yes | no | | In HTTP/2 & /3 | yes | forbidden | ## How to spot the confusion in an interview Candidates often say chunked "compresses the response in pieces", or that gzip is a transfer encoding. The clean framing to give is: *Content-Encoding changes what the bytes are; Transfer-Encoding changes how the bytes are delimited on one hop* — and in modern HTTP versions only the first still exists.

  • Can a single response carry both Content-Encoding and Transfer-Encoding?
    Yes, and it is common: a streamed gzipped response is `Content-Encoding: gzip` plus `Transfer-Encoding: chunked` in HTTP/1.1. They answer different questions — what the bytes are versus how they are delimited on this hop. What must never coexist is Content-Length and Transfer-Encoding: chunked.
  • Why does a message with both Content-Length and Transfer-Encoding: chunked have to be rejected?
    Because implementations disagree about which takes precedence. If a front-end proxy uses one and the back-end the other, the remainder of the body is interpreted as the start of a new request — HTTP request smuggling, which lets an attacker prepend requests to another user's connection. RFC 9112 requires rejecting such messages outright.
  • How is a body of unknown length sent in HTTP/2, where Transfer-Encoding does not exist?
    The body is simply a sequence of DATA frames on the stream, terminated when a frame carries END_STREAM. Length framing is a function of the binary protocol, so no chunked coding is needed; sending Transfer-Encoding in HTTP/2 is a protocol error.

Content-Encoding is vacuum-packing the goods inside the box — it stays packed all the way to the customer. Transfer-Encoding is how one courier straps the load onto their van; the next courier restacks it their own way.

saying these in an interview costs you the question

  • Describing chunked transfer encoding as a form of compression.
  • Calling gzip a transfer encoding, or claiming Content-Encoding is hop-by-hop.
  • Saying Transfer-Encoding: chunked still applies in HTTP/2 or HTTP/3.
  • Thinking Content-Length and chunked can both be present and one just wins.
  • Sharing one ETag across the gzip and Brotli variants of a resource.

context