Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

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.

Levels: Intermediate ~30 min read

Intermediate

What you will learn

  1. What Brotli is and why it wins (on the wire and in the CPU profile).

  2. The deployment reality: ngx_brotli — dynamic module, Module 15's build path, and the lesson's verified-instead configuration.

  3. The dual-stack pattern: brotli + gzip side by side, negotiation doing the routing.

  4. Static Brotli ( brotli_static) — the pre-compressed endgame for versioned assets.

  5. 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

br at level ~4

—

often beats gzip -9 smaller and faster

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:

http
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-Encoding

2. 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):

nginx
# 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):

text
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 noted

Two production notes on the stack:

  • Order is the preference, and yours is fixed: with brotli on, NGINX prefers br over gzip when 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, Vary keeps 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):

bash
# 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 disk

Rules 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:

  1. Model the negotiation. With curl --compressed (offers gzip, deflate, br, zstd — check curl -V), observe which Content-Encoding your 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.

  2. The ratio baseline. Record gzip's ratio on your fixtures — after Module 15's build, the same fixtures through br will show the 15–25% delta on the exact same numbers. A before/after experiment you own end-to-end.

  3. Static pre-compression rehearsal. gzip -9 -k a fixture in /var/www/, add gzip_static on; to a location, and fetch it — zero-CPU compressed serving, today. The brotli version is the same line with br.

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, Vary protects, gzip remains the universal fallback; CPU stays single-codec.

  • brotli_static + build-time q11 is the pre-compression endgame; mirrors gzip_static line-for-line.

  • When you can't have it: gzip discipline is the honest 90% — and you now hold both playbooks.

Was this page helpful?

Mark this learning resource complete to track your learning progress.