All articles
169 articles · updated weekly See our Tools
All articles
Comparisons

Gzip vs Brotli: which compression to use

Gzip vs Brotli: learn which compression to use, with practical examples, common mistakes, and what to watch for when applying it in production.

Gzip vs Brotli: which compression to use
COVER · Comparisons

I turned Brotli up to max and took the server down

I saw a benchmark saying Brotli at level 11 compresses much better than Gzip, figured it was just flipping a flag, and went for it. I turned on brotli on with quality 11 for all the dynamic traffic of an API. The result: p95 latency doubled and the server CPU pinned to the ceiling under load. The byte savings were real, but I was paying for them in CPU on every request, on content I couldn't even cache. The lesson wasn't "Brotli is bad" — it was that compression is a tradeoff between CPU and bandwidth, and that tradeoff changes depending on whether the content is static or dynamic.

Here I compare Gzip and Brotli from an operator's point of view, not from someone who just read a benchmark. Since compression and caching solve neighboring problems — one shrinks what you transfer, the other avoids transferring it again — it's worth reading alongside the post on browser caching with Cache-Control and ETag.

How HTTP compression works, fast

The mechanism is a negotiation. The client sends a header on the request saying what it accepts:

Accept-Encoding: gzip, br

The server picks an algorithm it understands, compresses the response, and returns it announcing what it used:

Content-Encoding: br
Vary: Accept-Encoding

br is Brotli's identifier; gzip is Gzip's. The Vary: Accept-Encoding header is important and frequently forgotten: it tells caches (CDN, proxy) that the response varies by encoding, so they don't serve a Brotli body to a client that only understands Gzip. Forgetting it hands unreadable bytes to a chunk of your users.

The real difference between the two

Gzip has been around since the 90s, is universal, and is fast. Brotli is newer (Google, ~2015), was designed for the web, and compresses fewer bytes than Gzip on text — HTML, CSS, JS, JSON. The typical difference is around 15% to 25% smaller for web assets at the max level.

The detail that changes everything is the cost curve. Brotli has 12 quality levels (0 to 11). Level 11 compresses the most, but it's dramatically slower than Gzip at the equivalent level — it can be an order of magnitude more expensive in CPU. Brotli's low levels, on the other hand, are fast and compress similarly to fast Gzip. In other words: "Brotli vs Gzip" isn't a single question. It's "high-level Brotli vs Gzip" and "low-level Brotli vs Gzip", and the answers are opposite.

Static vs dynamic: the decision that matters

This is the split that avoids the mistake I made. Content divides into two worlds, and each one calls for a different strategy.

Static is everything you generate once at build time and serve many times unchanged: the JS bundle, the CSS, text-format fonts. For this the strategy is precompression. You compress at the max level (Brotli 11) during the build, save the .br next to the file, and the server just delivers the ready-made file. The CPU cost is paid once, offline. Runtime latency is zero. It's the best of both worlds and there's no excuse not to do it.

# nginx serving precompressed files without compressing at runtime
brotli_static on;
gzip_static on;

Dynamic is what changes on every request: an API's JSON response, per-user rendered HTML. Here you can't precompress — you have to compress on the spot, inside the request window. The strategy is the opposite: low level. Brotli 4 or 5, or Gzip 5 to 6. You give up a few bytes to avoid blowing up the CPU under load. That's exactly where I went wrong: I used level 11 on dynamic content.

Don't compress what's already compressed

A silent and common mistake. Images (JPEG, PNG, WebP), video (MP4), audio, .zip, .gz files — all are already compressed in their own format. Running Gzip or Brotli over them doesn't reduce size (sometimes it adds a few bytes) and wastes CPU for nothing.

The rule is to compress only text-based content. Configure it by MIME type:

# compress text; leave compressed binaries alone
gzip_types text/plain text/css application/json
           application/javascript text/xml application/xml
           image/svg+xml;

SVG is included because it's text (XML). WebP and AVIF stay out because they're already compressed binaries. Telling the server to compress */* is an anti-pattern that costs CPU with no return.

What to actually configure

Putting it together, the setup I recommend for most projects:

  • Static assets: precompressed at build, Brotli 11 and Gzip 9 side by side, served with brotli_static/gzip_static. Zero runtime cost, maximum bandwidth savings.
  • Dynamic responses: runtime compression with Brotli at level 4–5 (or Gzip 5–6), only for text types. Balance between CPU and bandwidth.
  • CDN in front: if you use Cloudflare, Fastly, or similar, much of this is handled at the edge — they negotiate encoding, cache by Vary, and often precompress. Check what your CDN already does before duplicating the work at the origin.
  • Minimum threshold: don't compress tiny responses. Below ~1KB the encoding overhead isn't worth it; most servers have a gzip_min_length for this.

The CPU saved on dynamic content is usually worth more than the extra bytes — bandwidth is cheap, latency under load isn't. On static it's the reverse: compress without mercy, because the cost was already paid at build time.

Frequently asked questions

Is Brotli always better than Gzip?

In compression density for text, yes, at the same effort level. But "better" depends on the CPU cost. High-level Brotli is better for precompressed static; for dynamic content, low-level Brotli or even Gzip can be the right choice because it compresses fast. There's no single winner — it depends on static vs dynamic.

Do I need to disable Gzip if I enable Brotli?

No. Keep both. Not every client understands Brotli (old clients, some proxies), and Accept-Encoding lets the client choose. The server delivers Brotli to those who accept it and Gzip as a fallback. Serving both is the correct behavior.

Why doesn't compressing images help?

Because JPEG, PNG, WebP, and the like already apply image-specific compression internally. Gzip and Brotli are built for text and find no new redundancy in already-compressed data. The result is practically the same size with CPU wasted — sometimes the file even grows a few bytes because of the header.

Which Brotli level should I use for dynamic content?

Something between 4 and 5 in practice. That's where the compression/CPU ratio stays healthy for compressing within the request window without punishing latency. Level 11 is reserved for offline precompression of static assets, where CPU time doesn't count against the user.

What to take from this

Compression isn't a flag you flip and forget — it's a deliberate tradeoff between CPU and bandwidth, and the right answer depends on whether the content is static or dynamic. For static: precompress at the max level during the build and serve the ready file. For dynamic: low level, compressing on the fly, text only. Don't compress already-compressed binaries, don't forget Vary: Accept-Encoding, and keep Gzip and Brotli coexisting to cover all clients. The mistake that cost me a doubled p95 was treating everything as if it were the same thing.

RD
Author
Rafael Duarte
Backend developer with a background in fintech and B2B SaaS — worked on teams that scaled APIs from zero to millions of requests. Carries enough production scars to hold strong opinions on tools, patterns and architecture decisions. Not an academic: he read the UUID RFC when he had to choose between v4 and v7 for a write-heavy table.
View profile