gzip vs Brotli vs Deflate: Web Compression Algorithms Compared
Every engineer has flipped the same switch at some point. The dashboard says Content-Encoding: br, the bytes-over-the-wire number drops by a third, the Lighthouse score creeps up, and nobody has any idea what actually happened inside that switch. Compression is one of those parts of the web that quietly does enormous amounts of work and almost never gets read about in detail.
This is the deep version. What gzip, Brotli, and Deflate actually are. How they relate (they share more DNA than the names suggest). What the real compression ratio and speed numbers look like. Why Brotli usually wins on static assets but rarely on dynamic responses. And the server-side configuration that turns the right algorithm on for the right kind of content.
Table of Contents
- 1. Why This Question Keeps Coming Up
- 2. The Shared DNA: LZ77 + Huffman
- 3. Deflate: The Workhorse Hiding Inside Everything
- 4. gzip: Deflate Plus Framing
- 5. Brotli: What's Actually Different
- 6. Real Numbers
- 7. Dynamic vs Static: Why You Use Both
- 8. Browser Support and the HTTPS Quirk
- 9. Server Configuration
- 10. Deflate Inside ZIP and PDF
- 11. The Mistakes That Show Up in Production
- 12. Common Questions
- 13. Wrapping Up
Why This Question Keeps Coming Up
Web compression is one of the highest-leverage performance levers we have. A modern HTML document is mostly tag names, attribute keys, and English words. A JavaScript bundle is mostly the same hundred or so keywords repeated thousands of times. CSS is even worse, with the same selectors and property names appearing again and again. All of that redundancy is free for the algorithm to find and remove.
The catch is that compression is a budget. Smaller bytes over the wire cost more CPU on the server (and a little more on the client). Pick the wrong algorithm or the wrong level for the wrong type of content and you can make a fast page slower, push CPU usage on your origin through the roof, or break a stale cache. This post is the model that makes those tradeoffs make sense.
If you have not read the lossy vs lossless deep dive, that is the companion piece. It covers what JPEG and MP3 are doing on the lossy side. This one stays entirely on the lossless side, where the math is harder but the rules are tighter.
Deflate: The Workhorse Hiding Inside Everything
Deflate is the algorithm that combines LZ77 with Huffman, defined in RFC 1951. By itself, Deflate has no header, no checksum, no filename, no nothing. It is the pure codec.
That purity is why Deflate ended up everywhere. It does the actual work inside gzip, ZIP archives, PDF streams (where it appears as the FlateDecode filter), PNG image data, zlib-wrapped streams, and the (mostly historical) Content-Encoding: deflate header. Every time you decompress one of those, the bytes flowing through your decompressor are Deflate.
The confusing part is naming. Three things in the same family share confusable names, and tutorials use them interchangeably even though they are not:
| Name | What it actually is | Defined in |
|---|---|---|
| Deflate | Raw LZ77 + Huffman bitstream. No framing. | RFC 1951 |
| zlib | Deflate stream with a 2-byte header and an Adler-32 checksum at the end. | RFC 1950 |
| gzip | Deflate stream with a richer header (magic bytes, optional filename, OS, timestamp) and a CRC-32 footer. | RFC 1952 |
The HTTP Content-Encoding: deflate header, historically, was supposed to mean zlib. A few early servers shipped raw Deflate instead, browsers had to handle both, and the resulting ambiguity is one of the reasons modern stacks just send gzip. If you see deflate on the wire today, it is almost always a misconfiguration. The standard answer for "compressed text on HTTP" is gzip or Brotli.
gzip: Deflate Plus Framing
gzip is what we actually use on the web. The compression core is plain Deflate. The framing adds the things you need to ship the bytes safely: a magic number so tools can detect the format, an optional filename, a modification time, an OS identifier, and a CRC-32 of the original data so you can verify the round trip.
gzip exposes nine compression levels, 1 through 9. Higher levels do more work to find longer LZ77 matches and produce more efficient Huffman trees. The decompressor does the same work regardless of level, which is the magic of the format: a level-9 file is not slower to decompress than a level-1 file. Only the encoder pays.
The honest summary of the level dial in practice:
- Level 1. Fast encoder, weakest compression. Useful for very dynamic responses where CPU matters more than bytes.
- Level 6. The default. The sweet spot for almost every server, and what most stacks ship.
- Level 9. Squeezes out the last few percent at large CPU cost. Worth it for pre-built static assets, rarely worth it for on-the-fly responses.
The reason gzip has stayed dominant for thirty years is that it is fast, it is simple, every browser since the mid-1990s supports it, and the format itself is so well-specified that incompatibilities essentially do not exist.
Brotli: What's Actually Different
Brotli was published by Google and Jyrki Alakuijala in 2013, with web text as the explicit target. It does not throw out the LZ77 + entropy template. It refines it in three specific ways that matter.
A built-in static dictionary
gzip starts every compression session with an empty dictionary. The first time it sees the string <!DOCTYPE html>, it has to encode every byte. Brotli ships with a built-in dictionary of about 120 KB containing the most common substrings observed in HTML, CSS, JavaScript, and many human languages. It does not have to "learn" <!DOCTYPE html> at all; the dictionary already contains it as a single short reference. For small files, where there is not enough data for LZ77 to build up its own dictionary, this is a huge advantage.
Second-order entropy coding
gzip uses Huffman coding, which treats each symbol independently. Brotli uses Huffman coding with context modeling: the probability table for the next symbol depends on the symbols just before it. After <a hr the next byte is almost certainly e, so the encoder uses a probability table where e is very cheap. After scr the next byte is overwhelmingly i. This adds a small amount of state but extracts noticeably more redundancy from the symbol stream.
A bigger sliding window
Deflate's LZ77 window is fixed at 32 KB. Anything more than 32 KB back in the input is invisible to the back-reference logic. Brotli allows windows up to 16 MB. On a large bundled JavaScript file, where the same function or import statement might appear 200 KB later, Brotli can still find the match. gzip cannot.
Brotli also has more levels than gzip: 0 through 11. The shape of the curve is different. Levels 0 through 4 are roughly competitive with gzip in encoder speed and produce similar or slightly smaller output. Levels 5 through 9 trade encoder time for noticeably better ratios. Levels 10 and 11 are the "static asset" tier: slow enough that you would not run them per request, but the output is hard to beat for files you compress once and serve millions of times.
Decompression is symmetric the same way gzip is. A Brotli-11 file is not slower to decompress than a Brotli-4 file. The client only pays the encoder cost once, on the server, when the file is built.
Real Numbers
Compression numbers are squishy because they depend on the input. The numbers below are typical of published benchmarks (Google, Cloudflare, Akamai, and Squash) over the last several years for HTML, CSS, and JavaScript content. They are not promises about your codebase. They are the ballpark you should expect.
| Algorithm + level | Size vs original | Encoder cost | Decoder cost |
|---|---|---|---|
| gzip-1 | ~30% | Very fast | Very fast |
| gzip-6 (default) | ~25% | Fast | Very fast |
| gzip-9 | ~24% | Moderate | Very fast |
| Brotli-4 | ~22% | Fast | Fast |
| Brotli-6 | ~20% | Moderate | Fast |
| Brotli-11 | ~18% | Very slow | Fast |
Two patterns matter more than any specific number.
First, Brotli at equivalent CPU cost beats gzip on web text. Brotli-4 is in the same neighborhood as gzip-6 for encoder time and produces meaningfully smaller output. That is the comparison most teams should care about for dynamic responses.
Second, the gap between Brotli-11 and gzip-9 on static assets is large enough to matter for bandwidth bills but small enough that nobody would pay Brotli-11's encoder cost per request. The whole point of high-level Brotli is that you only run it at build time.
Dynamic vs Static: Why You Use Both
The single most useful mental model for web compression is to split content into two buckets and treat them differently.
Static assets are files you build once and serve millions of times. Your bundled JavaScript, your CSS, your fonts, your prerendered HTML. The encoder cost is paid once during the build. The decoder cost is paid by clients on every download, but it is small for both algorithms. Pre-compress these to Brotli-11 (and keep a gzip-9 copy for the rare client that does not support Brotli), store the compressed files on disk next to the originals, and let the server hand them out without re-compressing.
Dynamic responses are generated per request. JSON from your API, server-rendered HTML, anything personalized. The encoder cost is paid on every response, on your origin's CPU. Here the goal is the best ratio you can get inside a tight CPU budget. Brotli-4 or Brotli-5 is usually the sweet spot for HTML; gzip-6 is the safe fallback. You almost never want Brotli-11 on a dynamic response: the encoder time blows up your tail latency.
The rule I use: compress static at build time at the highest level you can afford, compress dynamic at request time at the level your CPU budget allows, and never mix them up. The mistake teams make most often is running Brotli-11 in front of a Node server that generates HTML, then wondering why the CPU graph is a wall.
Browser Support and the HTTPS Quirk
gzip is universally supported. Every browser, every HTTP client, every proxy released since the 1990s understands it. Nothing to think about.
Brotli is supported in every modern browser: Chrome, Firefox, Safari, Edge, and every Chromium-based browser since around 2016. By global usage, well over 95% of browser sessions accept it. The one quirk worth knowing: browsers only advertise Brotli on HTTPS connections. Over plain HTTP, browsers send Accept-Encoding: gzip, deflate and quietly drop the br. The reasoning was to avoid old corporate middleboxes mangling Brotli responses they did not recognize. The practical effect is that if you are not on HTTPS, Brotli does nothing for you, and that is one more reason to be on HTTPS.
For non-browser clients (mobile apps, server-to-server HTTP, scrapers), Brotli support varies. If you serve an API, the safe rule is to honor whatever the client advertises in Accept-Encoding and fall back to gzip when Brotli is absent.
Server Configuration
The simplest "good" configuration on Nginx, with the ngx_brotli module installed for dynamic Brotli and pre-built .br files on disk for static:
# gzip for the long tail of clients
gzip on;
gzip_comp_level 6;
gzip_min_length 256;
gzip_vary on;
gzip_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
application/atom+xml image/svg+xml;
# Brotli for everyone who advertises it
brotli on;
brotli_comp_level 5;
brotli_min_length 256;
brotli_types text/plain text/css text/xml application/json
application/javascript application/xml+rss
application/atom+xml image/svg+xml;
# Serve pre-compressed .br / .gz files for static assets
brotli_static on;
gzip_static on;A few things worth understanding about that config:
gzip_min_lengthandbrotli_min_lengthstop the server from compressing tiny responses. Below a few hundred bytes the framing overhead and CPU cost outweigh the savings.- The
_typesdirectives explicitly exclude images, video, fonts in already-compressed formats (woff2), and anything else that is already compressed. Running gzip on a JPEG wastes CPU and usually makes the file slightly larger. gzip_vary onemitsVary: Accept-Encoding, which is critical for CDN and proxy caches so they do not hand a Brotli response to a gzip-only client.brotli_static onmeans "if a request forapp.jsarrives andapp.js.brexists, serve that directly." This is how you cash in on Brotli-11 without spending the CPU at request time.
On Cloudflare and most managed CDNs, all of this is a checkbox. Brotli for dynamic content is on by default; Brotli for static assets is automatic for many MIME types. If your origin already sends Content-Encoding: br, the CDN will pass it through; if not, the CDN will compress on its end. The thing to confirm is that the CDN's Vary behavior and your origin's Vary behavior agree, because mismatches there are where stale-cache bugs come from.
Deflate Inside ZIP and PDF
The Deflate algorithm did not stop at HTTP. It is the default compression method (method 8) inside the ZIP file format, and it is by far the most common stream filter inside PDF files, where it shows up under the name FlateDecode.
When you create a ZIP file, the archive format is a directory of files plus per-file Deflate streams. When you compress a PDF, much of the size reduction comes from re-encoding the document's content streams through Deflate (or, for images, from re-encoding bitmaps through JPEG or other image codecs). PNG, similarly, runs row filters and then hands the result to Deflate.
The practical implication is that "make my ZIP smaller" and "make my PDF smaller" are not separate problems from "make my HTML smaller." They are the same problem under different framing. Newer ZIP and PDF tooling is gradually adding Zstandard and other modern codecs, but Deflate is still the format you can count on every reader supporting.
The Mistakes That Show Up in Production
1. Compressing already-compressed content
Running gzip or Brotli over a JPEG, MP4, woff2 font, or another .br file wastes CPU and almost always makes the file slightly larger because of the added framing. Filter your *_types lists to text and SVG and JSON. Let binary, image, and video formats pass through untouched.
2. High-level Brotli on dynamic responses
Brotli-11 takes orders of magnitude more CPU than Brotli-5 for a few percent of extra compression. On a static asset that is fine, you only pay once. On a dynamic response you pay on every request, and the tail latency goes through the roof. Keep dynamic Brotli at level 4 or 5 unless you have measured.
3. Forgetting Vary: Accept-Encoding
Without the Vary header, an intermediate cache (your CDN, a corporate proxy, a service worker) can hand a Brotli-compressed response to a client that does not understand Brotli, or hand a gzip response when the cache had a Brotli copy available. The symptom is a cryptic decoding error for some users and not others. Always emit Vary: Accept-Encoding on any response you compress conditionally.
4. Pre-compressing and re-compressing
Pre-built static assets shipped to a CDN that also compresses them is double work. The CDN reads your already-compressed file, decompresses it, and re-compresses with its own settings, which is usually worse than yours. Either ship raw files and let the CDN compress, or ship .br/.gz alongside the originals and configure the CDN to pass them through.
5. Treating Brotli as a silver bullet
The biggest savings in most apps are not from Brotli-vs-gzip. They are from minifying first, tree-shaking, removing unused CSS, and not shipping a 2 MB analytics blob in the first place. Compression is a multiplier on the bytes you ship. The cheapest byte is the one you do not ship at all.
Common Questions
Should I use Brotli everywhere?
For modern web traffic over HTTPS, yes, with gzip as a fallback. The handful of clients that do not support Brotli will fall back to gzip cleanly. There is no good reason to refuse Brotli to clients that ask for it.
Is Brotli decoding really slower than gzip?
Per byte processed, gzip's decoder is a touch faster. But Brotli's smaller output means there are fewer bytes to process. End-to-end "time until the browser starts parsing" is usually equal or slightly better with Brotli for typical web content. In benchmarks where you control the input size, gzip wins; in benchmarks where you compress real content and compare wall-clock time, Brotli usually wins.
What about Zstandard?
Zstandard (zstd) is Facebook's modern compressor and is excellent: better ratios than gzip across the board and configurable from "faster than gzip-1" to "competitive with Brotli-11." It is widely supported in tooling (Linux packages, file systems, S3, ZIP archives via newer specs), and HTTP support is growing: it is now in major browsers and most CDNs. For non-HTTP use cases (file storage, internal RPC, package compression), zstd is often the right answer today. For HTTP, gzip + Brotli is still the safest default; serve zstd in addition if your stack supports it.
Why does the deflate Content-Encoding exist if nobody uses it?
Historical mess. The HTTP spec said "deflate" meant zlib-wrapped Deflate. A few early servers shipped raw Deflate streams under the same name. Browsers learned to accept both, but neither version was clearly better than gzip, so the ecosystem standardized on gzip and quietly stopped using "deflate" on the wire. You will still see it in old documentation and the occasional misconfigured server.
Does compression hurt cache hit rates?
Only if you forget the Vary: Accept-Encoding header. With Vary in place, caches store one entry per supported encoding, and everything works. Without it, you get cross-encoding pollution and angry users with broken responses.
Wrapping Up
The web has spent thirty years polishing essentially the same idea: find repetition, encode it cheaply, send fewer bytes. gzip won the first long round because it was simple, fast, and good enough. Brotli won the second round on web text because it shipped a built-in dictionary, context modeling, and a bigger window, and because the modern web is mostly text. Both still build on the LZ77 and Huffman ideas from the 1950s through 1970s.
The decision rule that holds up in production is small. Compress text. Skip already-compressed content. Use Brotli-11 at build time for static assets, Brotli-4 or 5 at request time for dynamic responses, and keep gzip-6 as the fallback for clients that need it. Emit Vary: Accept-Encoding. Watch your CPU graph the first time you turn anything on, then forget about it.
If you want to see compression in action, the ZIP creator and PDF compressor on this site both run Deflate in your browser, with no upload. For more on the compression model behind these formats, the lossy vs lossless post is the companion to this one, and the UTF-8 guide covers the encoding layer that sits one step below compression.