Compression Algorithms Explained: gzip, Brotli, and the Trade-offs That Matter
Every text response your server sends can shrink to a fraction of its size before it hits the wire. The question isn't whether to compress — it's which algorithm, at which level, and where the CPU cost of compressing is worth paying.
Published July 6, 2026At its core, most general-purpose compression finds repeated patterns and replaces them with shorter references. LZ77-family algorithms, which gzip is built on, slide a window across the data looking for a sequence that already appeared earlier, and if it finds one, it emits a compact "go back N bytes, copy M bytes" instruction instead of the repeated bytes themselves. A Huffman coding pass then follows, replacing the most frequent remaining symbols with the shortest bit patterns and rarer ones with longer patterns, squeezing out further redundancy at the bit level. HTML, CSS, and JSON compress extremely well under this approach because they're full of repeated tags, keys, and whitespace patterns.
How the browser and server agree to compress
A client advertises what it can decode via a request header, and the server picks one and confirms it in the response:
Accept-Encoding: gzip, br
Content-Encoding: br
If the server ignores Accept-Encoding and sends compressed bytes the client never agreed to, the client can't decode the response at all. If it never compresses despite the client supporting it, bytes get wasted for no reason. This negotiation is the same content-negotiation mechanism HTTP uses for other things like language and media type, just applied to encoding.
Why Brotli usually wins on ratio
Brotli, developed for web content specifically, ships with a large built-in dictionary of common web strings — fragments of HTML boilerplate, common JavaScript tokens, frequent English words — that gzip doesn't have. For small responses especially, that shared dictionary means Brotli can reference a pattern it already knows about instead of having to have seen it earlier in the same response, which gzip has no way to do. In practice Brotli at a comparable setting typically produces files 15-25% smaller than gzip for text content, though the exact gap depends heavily on what's in the payload.
The cost side of the trade-off
Better compression ratio generally costs more CPU time to produce, and both gzip and Brotli expose a level knob that trades one for the other. gzip's levels run 1 (fast, weaker) through 9 (slow, better); Brotli's run 0 through 11, with the top few levels dramatically slower for only marginal size gains. The practical pattern most sites use: compress dynamically generated responses at a moderate level in real time, since every millisecond spent compressing delays the response, but precompress static assets once, at the highest quality level, at build or deploy time, since that CPU cost is paid once and then reused for every subsequent request.
# precompress a static build directory once, at max quality
brotli -q 11 -o app.js.br app.js
gzip -9 -k app.js
Compression doesn't help everything
JPEG images, MP4 video, and most already-compressed binary formats are close to their information-theoretic minimum size already; running gzip or Brotli over them again wastes CPU for a fraction of a percent of savings, sometimes even growing the file slightly due to compression overhead. Serving these with Content-Encoding set is close to pointless. The real wins are concentrated in text: HTML, CSS, JavaScript, JSON, and API responses, where redundant structure is everywhere and compression ratios of 70-90% size reduction are routine.
A sane default for most stacks
Serve Brotli when the client supports it, fall back to gzip for the clients that don't, and precompress every static asset at build time rather than compressing on every request. For dynamically generated responses that can't be precomputed, pick a moderate compression level rather than the maximum — the jump from level 6 to level 11 on Brotli buys a small size reduction for a large latency cost, which is the wrong trade for anything generated per-request.
A subtle security wrinkle: compression can leak information
Compressing a response that mixes secret data with attacker-influenced data can, in specific circumstances, leak the secret one byte at a time. The underlying mechanism is that compression ratio depends on how much repetition exists in the input, so if an attacker can control part of what gets compressed alongside a secret (a CSRF token reflected into a page alongside search input, for instance) and can observe the resulting compressed size, a correct guess at the next character of the secret compresses slightly better than a wrong guess, because it creates a longer repeated match. This was demonstrated publicly against TLS-level compression years ago and led most browsers and servers to disable compression at that layer entirely; the lesson that outlived the specific incident is to avoid compressing responses that mix a secret with attacker-reflected content in the same response body, regardless of which layer does the compressing.
Compression also interacts with range requests
A client asking for bytes 500-999 of a large file, using an HTTP range request to resume an interrupted download or stream part of a video, needs those byte offsets to refer to the same underlying file the client already has a partial copy of. If a server compresses the response on the fly and applies range semantics to the compressed bytes rather than the original file, resuming a download that started before compression was enabled, or that hit a different compression level, can silently produce a corrupted result. This is one of several reasons static assets are usually precompressed once, as fixed files with a fixed byte layout, rather than compressed fresh on every request that might specify a range.