Content-Encoding
The HTTP header naming the content codings applied to a body (gzip, br, zstd), which the recipient must undo to recover the media type in Content-Type. Chosen by Accept-Encoding negotiation; not Transfer-Encoding; cached variants stay apart only via Vary: Accept-Encoding.
Full Explanation
Content-Encoding is the HTTP header that names the content codings applied to a message body. This is beyond any encoding already inherent in the media type. A recipient must undo this encoding "in order to obtain data in the media type referenced by the Content-Type header field" (RFC 9110, section 8.4). Its purpose is narrow: it exists "to allow a representation's data to be compressed without losing the identity of its underlying media type". It is not Transfer-Encoding. Transfer-Encoding "is a property of the message, not of the representation". Any recipient along the chain may decode it or add it (RFC 9112, section 6.1). It does not replace Content-Type. Content-Type keeps describing the decoded data. A content coding is not always compression. The same IANA registry that lists gzip, br and zstd also lists aes128gcm. That is the encrypted content coding of RFC 8188. In everyday use, though, Content-Encoding is the server's half of an Accept-Encoding negotiation. It is the reason one URL can have several equally valid bodies. A shared cache must never mix these bodies up.
How it works
Content-Encoding is the answer to the request header Accept-Encoding. This is one axis of HTTP content negotiation:
- The client advertises what it can decode. "When sent by a user agent in a request, Accept-Encoding indicates the content codings acceptable in a response". "Each codings value MAY be given an associated quality value (weight)". So both
Accept-Encoding: gzip, br, zstdandAccept-Encoding: gzip;q=1.0, identity;q=0.5, *;q=0are legal. The token identity is "a synonym for 'no encoding'". "*" matches any coding not listed explicitly (RFC 9110, section 12.5.3). - The server picks a coding it can actually produce. Where more than one is acceptable, the rule is preference, not first match. "When selecting between multiple content codings that have the same purpose, the acceptable content coding with the highest non-zero qvalue is preferred". A qvalue of 0 means not acceptable. A request with no
Accept-Encodingat all makes any coding acceptable (RFC 9110, section 12.5.3). - The server labels the result. If several codings were applied, the sender MUST list them "in the order in which they were applied". The recipient therefore undoes them from the right-hand end. The coding named identity "is reserved for its special role in Accept-Encoding and thus SHOULD NOT be included" in
Content-Encoding(RFC 9110, section 8.4). A response carryingContent-Encoding: brwithContent-Type: application/javascriptis Brotli-compressed JavaScript. It is not a new media type. - If nothing acceptable can be produced, plain bytes are the fallback. "The origin server SHOULD send a response without any content coding unless the identity coding is indicated as unacceptable" (RFC 9110, section 12.5.3). Omitting the header is how a server says "uncompressed". Identity is never sent as a value.
- The same header can describe a request body. A client may compress what it uploads. An origin server "MAY respond with a status code of 415 (Unsupported Media Type) if a representation in the request message has a content coding that is not acceptable". Such a 415 ought to carry an
Accept-Encodingresponse header. That header enumerates what the resource does accept, so the client can retry (RFC 9110, sections 8.4 and 12.5.3, which absorbed and obsoleted RFC 7694).
The legal values are whatever the IANA HTTP Content Coding Registry holds. It is not a fixed list. gzip uses LZ77 with a 32-bit CRC. br is Brotli (RFC 7932): "a lossless compressed data format that compresses data using a combination of the LZ77 algorithm and Huffman coding". zstd is Zstandard. Its HTTP coding is capped at an 8 MB window: "decoders MUST support a Window_Size of up to and including 8 MB, and encoders MUST NOT generate frames requiring a Window_Size larger than 8 MB" (RFC 9659). The registry also holds the older deflate (zlib-wrapped) and compress, plus the deprecated aliases x-gzip and x-compress. Since September 2025 it also carries dcb and dcz. These are the Dictionary-Compressed Brotli and Zstandard codings of RFC 9842. The client names an already-fetched dictionary in an Available-Dictionary request header. It adds dcb or dcz to Accept-Encoding only when it holds a matching dictionary. A cacheable response "MUST include a Vary header" such as "Vary: accept-encoding, available-dictionary".
One consequence is easy to miss: "the representation is defined in terms of the coded form, and all other metadata about the representation is about the coded form" (RFC 9110, section 8.4). So Content-Length, ETag and byte ranges all describe the compressed bytes. They do not describe the original file.
Here is what one exchange looks like on the wire.
// Client request
GET /bundle.js HTTP/1.1
Host: cdn.example.com
Accept-Encoding: gzip, br, zstd
// Server response (Brotli chosen)
HTTP/1.1 200 OK
Content-Encoding: br
Content-Type: text/javascript
Content-Length: 14829
Vary: Accept-Encoding
Cache-Control: max-age=31536000
Why it matters for a CDN
Text formats compress several-fold. The saving lands on the last mile a CDN owns. This makes edge compression one of the largest byte reductions available. The cost is that one URL now has a family of bodies: gzip, Brotli, Zstandard, and uncompressed. These bodies are not interchangeable. The response must therefore carry Vary: Accept-Encoding. A cache "MUST NOT use this response to satisfy a later request unless the later request has the same values for the listed header fields as the original request [...] or reuse of the response has been validated by the origin server. In other words, Vary expands the cache key required to match a new request to the stored cache entry" (RFC 9110, section 12.5.5). RFC 9110 words the obligation as a SHOULD on origin servers. For a compressed response, treat it as mandatory, because the failure mode is not a slow page. It is an undecodable one: a stored Brotli body handed to a client that only speaks gzip, or to one that asked for no coding at all. Vary is also a cost: every distinct Accept-Encoding string is potentially another stored variant. That is why CDNs normalize the header before it reaches the cache key. It is also why an edge can legitimately recode a response at all: "A proxy MAY transform the content of a message that does not contain a no-transform cache directive". A proxy MUST NOT transform one that does (RFC 9110, section 7.7; the directive is defined in RFC 9111, section 5.2.2.6). So "cache-control: no-transform" is the origin's way to withdraw that permission. Because "frequently, the representation is stored in coded form, transmitted directly, and only decoded by the final recipient" (RFC 9110, section 8.4.1), an edge can compress once. It can store the coded form and spend no CPU on later hits. That is the difference between per-request and per-object compression cost.
What CDNs do
- Cloudflare "supports Gzip, Brotli, and Zstandard compression when delivering content to website visitors". It chooses between them, or no compression, based on the visitor's accept-encoding value, the zone plan, and any matching Compression Rule. The documented plan defaults are Zstandard on Free, Brotli on Pro and Business, and Gzip on Enterprise. Compression Rules are the documented way to enable Zstandard or override the default elsewhere. It compresses only an allowlist of content types, and only responses of at least 48 bytes for Gzip or 50 bytes for Brotli and Zstandard. Among successes it compresses only status 200, and among errors only 403 and 404. Toward the origin it always asks for "accept-encoding: br, gzip", regardless of what the visitor sent. It will convert between compressed and uncompressed forms independently of caching (Cloudflare, Content compression).
- Amazon CloudFront compresses with Gzip and Brotli (zstd is not documented). This happens once "Compress objects automatically" is set on the cache behavior and both formats are enabled in a cache policy. Brotli requires a cache policy, since it "doesn't support legacy cache settings". Enabling compression automatically adds
Accept-Encodingto the cache key and to origin requests. It compresses only allowlisted content types, only objects "between 1,000 bytes and 10,000,000 bytes in size", and only when the origin supplied a usable Content-Length. It compresses only for status 200, 403 or 404. "When the viewer supports both Gzip and Brotli, CloudFront uses Brotli". But the first cached variant wins afterwards: a cached Gzip object "will always return the Gzip version, even if the viewer accepts both Brotli and Gzip". If the origin already sentContent-Encoding, CloudFront never recompresses. HTTP/1.0 requests loseAccept-Encodingand are not compressed. Compression is best-effort and is skipped under high load (AWS, Serve compressed files). - Fastly compresses at the edge with GZip or Brotli in two distinct modes. Static compression runs pre-cache, when the response arrives from origin, and is available in VCL delivery services only. It is enabled through the UI, API, or the beresp.gzip and beresp.brotli variables, and it bills on the compressed size. Dynamic compression runs post-cache, as the response leaves, and works on VCL and Compute. It is switched on by setting an "X-Compress-Hint: on" response header in vcl_deliver, and it bills on the uncompressed size. Fastly also "automatically normalizes this header value to reduce the number of permutations, so that if the server delivers a compressed response which we can cache, we can reuse that response for as many users as possible". So "gzip, br" and "gzip, br, deflate" share one variant. In VCL services the edge cannot decompress an origin response. The origin must still be able to serve uncompressed bytes (Fastly, Delivering compressed content).
- Akamai separates serving from compressing. With the Brotli Support behavior, "the CDN serves Brotli-compressed assets from your origin server and caches them on Akamai edge servers". But it "doesn't compress resources within the Akamai CDN in real-time". The origin must produce the Brotli, and only compression level 6 is supported. If a gzip copy is already cached, "the gzip resource is served to save time and bandwidth" until that object's TTL expires. To compress at the edge, you add Adaptive Acceleration in an Ion property. Adaptive Acceleration's Brotli Compression "takes resources or gzip resources from your origin, applies Brotli compression, and then caches and delivers the compressed resources to requesting browsers" (Akamai, Brotli Support; Akamai, Adaptive Acceleration).
Watch out for
- Confusing it with Transfer-Encoding. Content-Encoding describes the representation and survives end to end. Transfer-Encoding describes one message on one hop, and any recipient may add or strip it (RFC 9112, section 6.1). "Transfer-Encoding: gzip, chunked" is not a Content-Encoding. Content-Type never changes because a body was compressed.
- Writing identity into Content-Encoding. It is reserved for
Accept-Encodingand SHOULD NOT appear as aContent-Encodingvalue. The correct way to say "uncompressed" is to omit the header (RFC 9110, section 8.4). - Re-compressing already-compressed formats. "If the media type includes an inherent encoding, such as a data format that is always compressed, then that encoding would not be restated in Content-Encoding" (RFC 9110, section 8.4). Fastly's guidance is to "only compress formats that are not already compressed (media formats like images, audio and video are typically already compressed and will not benefit from GZip or Brotli)". JPEG, PNG, WebP, MP4 and most fonts in WOFF2 spend CPU for no bytes.
- Tiny bodies. Thresholds are vendor policy, not a protocol rule. Cloudflare will not compress below 48 bytes for Gzip or 50 bytes for Brotli and Zstandard. CloudFront compresses only from 1,000 bytes up.
- Metadata that moves when the edge compresses. Cloudflare "may omit the Content-Length HTTP header" on responses it transforms. Preserving it means adding "cache-control: no-transform" at the origin. CloudFront "converts the strong ETag header value to a weak ETag" when it compresses a strongly validated object. This keeps compressed and uncompressed forms usable for conditional requests. Anything that depends on byte-exact length or strong validators needs no-transform.
- Vary drift. A stripped, wildcarded, or overly wide Vary is a correctness bug. Without
Accept-Encodingin it, caches serve the wrong coding. With the raw client header in it, each browser spelling becomes its own variant, and hit ratio falls. - Compression as a side channel. BREACH-style attacks "make similar use of HTTP-level compression to decrypt secret data passed in the HTTP response". There is no TLS layer fix. Mitigations are application-level, such as randomising CSRF tokens (RFC 7457, section 2.6). Do not compress responses that mix a secret with attacker-influenced input.
- Coding availability is not universal. AWS notes that Chrome and Firefox offer Brotli only over HTTPS. Some non-conformant senders emit raw deflate without the zlib wrapper the coding requires (RFC 9110, section 8.4.1.2). That is why deflate is best avoided in favour of gzip or br.
Best practice
- Send
Vary: Accept-Encodingon every cacheable compressible response. Make sure no edge configuration strips it. It is what keeps the gzip, Brotli, and uncompressed variants apart in every shared cache. - Compress text-like types only: HTML, CSS, JavaScript, JSON, XML, SVG, plain text. Leave images, audio, video, and WOFF2 alone.
- Prefer compress-once for popular static assets. Use pre-compressed files at the origin, or the CDN's pre-cache mode, such as Fastly's static compression or CloudFront's cached compressed object. Fastly calls static compression "the most efficient way to compress data at the edge, especially for cacheable responses": the cached object serves later requests "without having to perform the compression again".
- Let the edge normalize
Accept-Encoding, or normalize it yourself to a small set such as br, gzip, and none. Variant count, not compression ratio, is what usually costs hit ratio. - Keep conditional requests working. Accept weak ETags on compressed variants. Do not pin a Content-Length at the origin for responses you allow the edge to transform.
- Verify the negotiation rather than assuming it. Ask for br, then gzip, then identity. Check that the returned
Content-Encodingmatches, that Vary is present, and that the decoded body is byte-identical to the original. The curl checks below do exactly that.
Interactive Animation
Examples
You can test compression support with curl. The output shows Content-Encoding at work.
# Request with Brotli support
$ curl -sI -H "Accept-Encoding: br" https://cdn.example.com/app.js | grep -i 'content-encoding\|content-length\|vary'
Content-Encoding: br
Content-Length: 14829
Vary: Accept-Encoding
# Request without compression
$ curl -sI -H "Accept-Encoding: identity" https://cdn.example.com/app.js | grep -i 'content-encoding\|content-length'
Content-Length: 52417
# No Content-Encoding header = no compression
# Notice the size difference: 14KB compressed vs 52KB uncompressed
Frequently Asked Questions
The HTTP header naming the content codings applied to a body (gzip, br, zstd), which the recipient must undo to recover the media type in Content-Type. Chosen by Accept-Encoding negotiation; not Transfer-Encoding; cached variants stay apart only via Vary: Accept-Encoding.
You can test compression support with curl. The output shows Content-Encoding at work.
# Request with Brotli support
$ curl -sI -H "Accept-Encoding: br" https://cdn.example.com/app.js | grep -i 'content-encoding\|content-length\|vary'
Content-Encoding: br
Content-Length: 14829
Vary: Accept-Encoding
# Request without compression
$ curl -sI -H "Accept-Encoding: identity" https://cdn.example.com/app.js | grep -i 'content-encoding\|content-length'
Content-Length: 52417
# No Content-Encoding header = no compression
# Notice the size difference: 14KB compressed vs 52KB uncompressed