15.5 Brotli, Measured
The config was Lesson 9.4's and the build was 15.2/15.3's — this lesson is the wire's turn to talk: 285 bytes became 50 while gzip managed 77, and the fine print (HTTPS-only browser br, CDNs, streaming) turns into runbook material.
Advanced
What you will learn
The receipts: one fixture, four
Accept-Encodingoffers — 285 bytes became 50 under br, 77 under gzip, 285 uncompressed when nobody asks.The HTTPS asterisk: browser
brrides secure contexts only — what that does to your rollout bet, with Module 6's TLS termination as the prerequisite.The operational fine print: CDN re-compression, streaming/SSE and
brotli_window, and the tiny-JSON cutoff where neither codec pays.The measurement plan:
$gzip_ratiocan't tell the brotli story — bytes-out per content-type, golden-diffed pre/post, module pinned per Lesson 15.3.
1. The server under test
The build and load are done — 15.2 compiled the
.so pair, 15.3 made it binary-compatible, and 9.4 wrote the canonical config. What follows is the exact server that produced every number below:
server {
listen 9701;
root www/main;
brotli on;
brotli_comp_level 5;
brotli_types text/plain text/css application/json application/javascript image/svg+xml;
gzip on; # the dual stack
gzip_types text/plain text/css application/json application/javascript image/svg+xml;
}Receipt-honesty notes: the server above runs level 5 — inside 9.4's canonical 4–6 knee, one notch under the module's default 6 — with a fixture-short types list; production copies 9.4's block verbatim (level 6, the full list,
brotli_static on).
2. Negotiation, on the wire
The receipt table below (real captures — one fixture, four clients):
Accept-Encoding: 'br' -> br 50 bytes
Accept-Encoding: 'gzip' -> gzip 77 bytes
Accept-Encoding: 'gzip, br' -> br 50 bytes
Accept-Encoding: '' -> identity 285 bytesThe protocol reading is 9.4's routing table with bytes attached: no offer, no compression; either codec honors its own client; both offered → brotli. What the receipt adds is arithmetic — 50 vs 77 on the identical fixture is 35% under gzip, and 285 → 50 is 82% off the wire, the whole business case on one line. One honesty clause for the runbook: a 285-byte fixture is more repetitive than production payloads — budget 9.4's 15–25% band, then measure your own.
3. The operational fine print
Whether brotli is worth it is 9.4's decision table — unchanged. What the table can't carry is the fine print, and all of it is operational:
The HTTPS asterisk: browsers send
Accept-Encoding: bronly over secure contexts (the spec's restriction on the token) — plain-HTTP browser traffic negotiates gzip, and non-browser clients do as they please. Check your TLS share before betting the rollout; Module 6's termination is the prerequisite.CDNs re-compress at the edge anyway — origin brotli matters most when YOU are the edge.
Streaming/SSE: dynamic brotli buffers like gzip did — mind
brotli_window(default 512k; the directive accepts powers of two from 1k up to 16m — the format’s full ceiling, no pre-compression tricks needed), and consider identity for event streams.The measurement gap: Module 12's log fields carry
$gzip_ratioand nothing brotli-side — track bytes-out per content-type, golden-diffed pre/post rollout.The module stays a pinned dependency — repo + commit (
a71f931) + paired rebuild per nginx bump. Lesson 15.3's rule, one runbook line.
Try it yourself
curl -s -o /dev/null -w '%{size_download} bytes\n' -H 'Accept-Encoding: br' http://127.0.0.1:9701/— then re-run with 'gzip', 'gzip, br', and no header at all: the full negotiation table and its byte sizes are the expected result.Feel the level curve: set
brotli_comp_level 11, reload, re-measure the 50 bytes — did it move enough to justify the CPU? (On a 285-byte fixture: barely. On real assets: trywww/main/compressible.txt.)Pre-compressed assets:
brotli -q 11a CSS fixture to.br, drop it by the original, enablebrotli_static on;, and watch the static module serve it with zero request-path CPU.
Q&A
Q: Does brotli help API JSON or just web assets? A: JSON compresses excellently (repetitive keys) —
application/json in the types list is deliberate. For tiny JSON (<~1 KB, Module 9's min-length thinking), skip both codecs: per-response overhead beats the savings.
Q: HTTP/3 + brotli? A: Orthogonal stacks that compose: QUIC transport (Lesson 9.5) + brotli payload (this lesson) is the modern maximal shape. No special config needed beyond both being on.
Takeaways
285 → 50 bytes under br, 77 under gzip, 285 uncompressed when nobody asks — the negotiation table is a receipt you ran, not a diagram from 9.4.
One home per story: 9.4 owns the config, 15.2/15.3 own the build — this lesson only spent the capability.
Browser br is HTTPS-context traffic — Module 6's TLS termination is the prerequisite; non-browser clients do as they please.
The fine print is operational: CDNs re-compress, streaming wants small windows or identity,
$gzip_ratiowon't tell the brotli story — golden-diff bytes-out, and pin the module (15.3's rule).
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.