HTTP Compression: gzip, Brotli, zstd

gzip vs Brotli vs deflate vs zstd, the Accept-Encoding/Content-Encoding negotiation, what and when to compress, the BREACH/CRIME risks, and Content-Encoding vs Transfer-Encoding.

HTTP compression shrinks a response body to cut how much travels over the wire. Text like HTML, CSS, JS, and JSON is highly repetitive and typically shrinks by 70-90%, which materially affects page load speed and bandwidth cost. It works by client and server negotiating a supported scheme, the server compressing the body and stating which scheme it used, and the client decompressing to consume it.

Negotiation: Accept-Encoding and Content-Encoding

The client lists the schemes it can decompress in the Accept-Encoding request header; the server picks one, compresses the body, and declares the scheme it actually used in the Content-Encoding response header. That negotiation must be marked with Vary: Accept-Encoding so caches keep compressed and uncompressed copies apart.

GET /app.js HTTP/1.1
Host: example.com
Accept-Encoding: br, gzip, deflate

HTTP/1.1 200 OK
Content-Type: application/javascript
Content-Encoding: br
Vary: Accept-Encoding
Content-Length: 24518

Comparing the algorithms

The common algorithms strike different balances between ratio and speed.

What and when to compress

Compression isn't free or universal. Don't recompress already-compressed binaries. Files like JPEG, PNG, WebP, MP4, zip, and gz are internally compressed already; re-compressing barely helps (or grows the file) while wasting CPU. Compression pays off on text: HTML, CSS, JS, JSON, XML, SVG, and some fonts. Also, very small responses (a few hundred bytes) let the compression overhead cancel the gain, so set a minimum-size threshold around 1KB and compress only text above it.

Security risks: CRIME and BREACH

Compression carries a subtle security risk. Because compressed output shrinks more when data repeats, an attacker who tweaks a request and watches the resulting response (or request) size can guess a secret inside it (a session or CSRF token) one character at a time. CRIME targeted request-header compression (TLS/SPDY level); BREACH targets HTTP response-body compression. Defenses include (1) not reflecting a sensitive secret into a compressed response body alongside user input, (2) randomizing (masking) the CSRF token per request, and (3) hiding length via random padding. These application-level measures are more practical than turning gzip off entirely.

Content-Encoding vs Transfer-Encoding

Two easily confused headers must be told apart. Content-Encoding means the resource's representation itself is compressed — an end-to-end property. What a cache stores is the compressed body, and the ETag reflects the compressed representation. In contrast, Transfer-Encoding: chunked (or gzip) is a hop-by-hop property describing only how the body is framed on this connection, used to send a body in pieces when its length isn't known up front. For real-world web performance, compression is almost always Content-Encoding, while Transfer-Encoding is best understood as a streaming transfer mechanism.