gzip vs Brotli vs Deflate: Web Compression Algorithms Compared

Rahmat Ullah profile photoRahmat Ullah
13 min readDeveloper Tools, Web, Compression

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.

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.

The Shared DNA: LZ77 + Huffman

gzip, Brotli, Deflate, ZIP, PDF streams, PNG, and a dozen other formats all stand on the same two ideas. Once you understand those, the rest of the family stops looking mysterious.

LZ77, published by Lempel and Ziv in 1977, is the dictionary half. The algorithm slides a window across the input looking for repeated byte sequences. When it finds one, it writes a back-reference instead of the bytes: go back 1,240 bytes, copy 28 bytes. The first occurrence of every string is paid for in full; every subsequent occurrence is a tiny pointer. On a file where the same word appears a hundred times, you pay for it once.

Huffman coding, from David Huffman's 1952 PhD work, is the entropy half. It assigns shorter bit patterns to common symbols and longer patterns to rare ones, so the symbols themselves get cheaper. In English text, the letter e might end up as 3 bits and the letter q as 12 bits, and the average across the file goes down.

Almost every general-purpose lossless compressor on the web does these two things in some order. LZ77 first to remove structural redundancy, then Huffman or one of its descendants to compress the resulting symbol stream. What separates gzip from Brotli is mostly how clever each one is at each step, not which steps it runs.

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:

NameWhat it actually isDefined in
DeflateRaw LZ77 + Huffman bitstream. No framing.RFC 1951
zlibDeflate stream with a 2-byte header and an Adler-32 checksum at the end.RFC 1950
gzipDeflate 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 + levelSize vs originalEncoder costDecoder cost
gzip-1~30%Very fastVery fast
gzip-6 (default)~25%FastVery fast
gzip-9~24%ModerateVery fast
Brotli-4~22%FastFast
Brotli-6~20%ModerateFast
Brotli-11~18%Very slowFast

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_length and brotli_min_length stop the server from compressing tiny responses. Below a few hundred bytes the framing overhead and CPU cost outweigh the savings.
  • The _types directives 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 on emits Vary: 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 on means "if a request for app.js arrives and app.js.br exists, 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.