9.4 Brotli and Modern Compression
Brotli typically beats gzip 15–25% on text and costs nothing to serve to clients that can’t speak it. The catch: it’s a dynamic module, so the edge must load it first — receipts included.
Intermediate
What you will learn
What Brotli is and why it wins (on the wire and in the CPU profile).
The deployment reality:
ngx_brotli— dynamic module, Module 15's build path, and the lesson's verified-instead configuration.The dual-stack pattern: brotli + gzip side by side, negotiation doing the routing.
Static Brotli (
brotli_static) — the pre-compressed endgame for versioned assets.The decision table: when brotli pays, when gzip remains enough.
1. The case for Brotli
Brotli (Google, 2013–2015, shipped in every modern browser) is a DEFLATE successor with a bigger, smarter toolbox:
gzip (DEFLATE) | Brotli | |
|---|---|---|
Window | 32 KB | up to 16 MB (finds matches across big files) |
Dictionary | none | 120 KB built-in of common web strings (html/js/css tokens) |
Huffman | static/dynamic | context-modeled, per-block-type |
Typical text result | baseline | 15–25% smaller at comparable CPU |
| — | often beats
|
Clients | everything since 1998 | all modern browsers; curl ≥ 7.57; not ancient appliances |
For a text-heavy site, brotli is a straight upgrade for ~95% of traffic — with gzip remaining as the universal fallback. The wire cost is one header of negotiation:
GET /app.js
Accept-Encoding: gzip, deflate, br ← client ranks its codecs
HTTP/1.1 200 OK
Content-Encoding: br ← server picked the best offer
Vary: Accept-Encoding2. The deployment reality
Stated plainly:
ngx_brotli
is a third-party module — the official nginx.org packages don't ship it, so
brotli on; fails at
nginx -t here exactly as it does on most stock installs. The path to running it is Module 15's dynamic-module curriculum: build
ngx_brotli as an
.so against your exact binary (
--with-compat matters),
load_module, done. The scaffolding you get
today is the config that lights up the moment the module loads: the dual-stack locations and the negotiation routing, ready to reuse.
The configuration you'll run once it does — presented here as the canonical shape (this is the config Module 15's build will light up):
# after Module 15: load_module modules/ngx_http_brotli_filter_module.so;
# load_module modules/ngx_http_brotli_static_module.so;
brotli on;
brotli_comp_level 6; # ~4-6 is the knee; br-6 ≈ gzip-9 size, gzip-6 CPU
brotli_types text/plain text/css text/javascript application/json
application/javascript application/xml image/svg+xml;
brotli_static on; # serve .br files from disk when present
gzip on; # the fallback stack stays armed
gzip_comp_level 6;
gzip_types (same list);
gzip_vary on;The directive vocabulary is deliberately gzip-mirrored (
brotli_types,
brotli_min_length,
brotli_static,
brotli_comp_level) — everything you know transfers one-to-one, which is the mark of a well-designed module.
3. The dual-stack pattern
With both codecs armed, NGINX's filter chain negotiates per request — the pattern to internalize (with gzip standing in for brotli until Module 15's build):
client sends Accept-Encoding: br → Brotli response (Content-Encoding: br)
client sends Accept-Encoding: gzip → gzip response
client sends gzip, br (ranked) → brotli wins (preferred codec)
client sends none (ancient/probe) → identity, Vary notedTwo production notes on the stack:
Order is the preference, and yours is fixed: with brotli on, NGINX prefers
brovergzipwhen the client offers both — the correct default (smaller wire), and the reason you can deploy brotli with zero risk to gzip-only clients: negotiation routes them,Varykeeps caches honest, nobody gets bytes they can't decode.CPU symmetry: both codecs in the chain means one runs per response, not both (filters short-circuit on the chosen encoding). Your CPU budget stays single-codec; the dual stack is a routing decision, not a doubling.
4. Static Brotli, concretely
The deploy pipeline shape (works identically for
gzip_static, shown brotli-side since it shines there):
# build step (per release):
brotli -q 11 -k -f dist/app.js # writes dist/app.js.br
gzip -9 -k -f dist/app.js # writes dist/app.js.gz (fallback)
# NGINX:
brotli_static on; # request /app.js with AE:br -> serve app.js.br from disk
gzip_static on; # request /app.js with AE:gzip-> serve app.js.gz from diskRules that make statics safe: there is no mtime guard — the static module serves whatever
.br is on disk; freshness is the pipeline's contract (deploy source and artifact atomically); the extension is stripped for the client (
/app.js is the URL,
.br never appears); and versioned filenames (the Module 8 key-versioning habit) make the whole invalidation question moot.
5. The decision table
Your situation | Call |
|---|---|
Modern-audience web frontends | Brotli dual-stack; static-br for hashed assets at q11 |
Mixed fleet with appliances/old SDKs | Brotli dual-stack — negotiation protects the legacy lane |
CPU-starved edge, cold dynamic text | gzip level 1–4, or brotli 4 (comparable CPU, smaller) |
Mostly binary/media traffic | Neither — compression budget ≈ zero (Lesson 9.3's table) |
Can't install modules on the edge | gzip discipline (Lesson 9.3) is 90% of the win; pre-gzip statics |
No brotli module loaded (yet) | Study the config, then Module 15 builds it for real |
Try it yourself
The gzip side of every experiment in this lesson runs today — the brotli side lights up after Module 15:
Model the negotiation. With
curl --compressed(offers gzip, deflate, br, zstd — checkcurl -V), observe whichContent-Encodingyour server picks today; then send explicit subsets:-H 'Accept-Encoding: br'(identity — no br support yet),-H 'Accept-Encoding: gzip'(gzip). You've just walked the dual-stack routing table with gzip as the stand-in.The ratio baseline. Record gzip's ratio on your fixtures — after Module 15's build, the same fixtures through
brwill show the 15–25% delta on the exact same numbers. A before/after experiment you own end-to-end.Static pre-compression rehearsal.
gzip -9 -ka fixture in/var/www/, addgzip_static on;to a location, and fetch it — zero-CPU compressed serving, today. The brotli version is the same line withbr.
Q&A
Q1. Client sends
Accept-Encoding: gzip, br. What's served, and why is that safe for a gzip-only client?
Answer
Brotli (preferred when offered). The gzip-only client never offers
br, so negotiation routes it to gzip —
Vary: Accept-Encoding keeps caches from crossing the populations. Zero-risk dual stack.
Q2. Why are Brotli levels 10–11 absent from the runtime knee but perfect for build pipelines?
Answer
They're 10–100× slower for ~5–15% extra shrink — irrational per request, trivial per deploy.
brotli_static serves their output from disk at zero runtime CPU: precompute the expensive, serve the immutable.
Q3. Your edge can't load third-party modules. What's the honest compression ceiling?
Answer
Lesson 9.3's full gzip discipline — types coverage, level knee,
gzip_static pre-compression at level 9. That's most of the practical win; brotli's incremental 15–25% on text waits for a module-capable edge (Module 15 makes you module-capable).
Q4. A stale
.br file sits next to a fresh source file. What does
brotli_static do?
Answer
Serves the stale
.br anyway —
brotli_static has no mtime guard: it serves whatever
.br file exists, whenever it exists. Staleness is the build pipeline’s job — deploy source and pre-compressed artifacts atomically (or version the filenames).
Takeaways
Brotli: 15–25% smaller than gzip on text at comparable CPU, built-in web dictionary, 16 MB window — preferred whenever the client offers it.
It's a third-party module (
ngx_brotli): Module 15's dynamic-module build is the on-ramp; the config shape (this lesson) is ready for it.Deploy as a dual stack — negotiation routes,
Varyprotects, gzip remains the universal fallback; CPU stays single-codec.brotli_static+ build-timeq11is the pre-compression endgame; mirrorsgzip_staticline-for-line.When you can't have it: gzip discipline is the honest 90% — and you now hold both playbooks.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.