Hands-on nginx gzip on/off lab: HTML ~107x smaller at level 1; JSON RPS ~1987 off vs ~1031 l1 vs ~563 l6 on localhost. Measured numbers, no Docker.
6 min read
gzip on; looks free until you measure it on a fast path. On a slow WAN, smaller wire bytes win. On localhost (and many LAN/VPC hops), the CPU that builds those bytes can dominate client RPS.
This is a hands-on lab with measured numbers:
gzip off, gzip_comp_level 1, gzip_comp_level 6.Content-Encoding for HTML, JSON, and random binary.Accept-Encoding: gzip.application/octet-stream never got a Content-Encoding.Related links:
Lab honesty (30 Sep 2026 IST): Shared Linux lab box (8 cores, kernel 6.12). nginx/1.26.3 host binary. Three masters on 127.0.0.1:18120–18122 only. Payloads: page.html 159 279 B, data.json 443 791 B, noise.bin 524 288 B random. Client: Python 3.13.5 urllib thread floods, Connection: close. No Docker. No public bind. Affiliates: 0.
Verdict up front: repetitive HTML/JSON compressed ~107× / ~38× at level 1, but JSON floods dropped from ~1987 RPS (off) to ~1031 (l1) and ~563 (l6) on loopback. Gzip is a bandwidth vs CPU trade — measure both.
With gzip on and a matching gzip_types / default HTML behavior, nginx may compress the response body when the client sends Accept-Encoding: gzip. gzip_comp_level picks the zlib effort. gzip_min_length skips tiny bodies. Types you omit stay uncompressed even if the client asks for gzip.
Related links:
| Knob | Lab setting |
|---|---|
| off | gzip off on :18120 |
| level 1 | gzip on; gzip_comp_level 1 on :18121 |
| level 6 | gzip on; gzip_comp_level 6 on :18122 |
| min length | 64 bytes |
| types | text/css, text/plain, application/json, JS, XML (+ default HTML) |
| File | Off (B) | Level 1 (B) | Level 6 (B) | Ratio l1 |
|---|---|---|---|---|
page.html | 159 279 | 1 491 | 1 282 | ~106.8× |
data.json | 443 791 | 11 584 | 12 222 | ~38.3× |
noise.bin | 524 288 | 524 288 | 524 288 | 1.0× (not gzipped) |
index.html | 3 | 3 | 3 | below gzip_min_length |
noise.bin is application/octet-stream — not in our gzip_types, so no Content-Encoding even with Accept-Encoding: gzip. That is the correct MIME lesson, not a bug.
Odd but measured: for this JSON, zlib level 1 produced a smaller body than level 6 (11 584 vs 12 222). We reproduced the same ordering with Python gzip.compress. Higher gzip_comp_level is not a guaranteed size win — and it cost a lot of CPU below.
| Arm | Path | Wire p50 (B) | Client RPS | Lat p50 |
|---|---|---|---|---|
| off | /data.json | 443 791 | ~1987 | 8.0 ms |
| level 1 | /data.json | 11 584 | ~1031 | 18.5 ms |
| level 6 | /data.json | 12 222 | ~563 | 32.9 ms |
| off | /page.html | 159 279 | ~2123 | 7.6 ms |
| level 1 | /page.html | 1 491 | ~2295 | 5.8 ms |
| level 6 | /page.html | 1 282 | ~1469 | 12.5 ms |
On loopback there is almost no WAN to save. Compressing a ~434 KiB JSON cut RPS roughly 2× at level 1 and 3.5× at level 6. The tiny HTML payload at level 1 was the one case where RPS ticked up slightly — wire shrank enough that transfer+parse won over compress CPU.
Clients that sent Accept-Encoding: identity against the gzip-on masters still got uncompressed bodies (and we saw Vary: Accept-Encoding on the gzip arms).
This lab is hostile to gzip on purpose: localhost, Connection: close, CPU on the same box as the client. On real edges — mobile, cross-region, expensive egress — a 38× JSON shrink dominates. Prefer:
gzip_comp_level 1–3 for dynamic JSON/HTML on fast links.gzip_static for cold assets when you can.Related links:
gzip_comp_level ⇒ smaller — our JSON said otherwise; always weigh size and RPS.gzip_types (plus default HTML) participate.Vary: Accept-Encoding — caches must key on it.gzip_min_length correctly skipped our 3-byte index.gzip_comp_level 1 for dynamic responses; raise only after measuring size and CPU/RPS.Content-Encoding: gzip with a real Accept-Encoding probe — not just a green config test.gzip_static — different knobs, not measured here.Gzip crushed wire size for repetitive HTML/JSON (~107× / ~38× at level 1) but halved to thirded JSON client RPS on localhost versus gzip off. Level 6 was worse on both size (for our JSON) and RPS than level 1. Use gzip where bandwidth matters; keep the level modest on dynamic origins; measure MIME coverage so you are not compressing — or failing to compress — by accident.
Evidence path on the lab box: lab-evidence/20-nginx-gzip/results/. Affiliates: 0.
Lab evidence
Lab 30 Sep 2026 IST. nginx/1.26.3 on 127.0.0.1:18120–18122 (off / level 1 / level 6). page.html 159279→1491 (l1) / 1282 (l6); data.json 443791→11584 (l1) / 12222 (l6) — zlib level 1 smaller than 6 on this JSON; noise.bin 524288 unchanged (octet-stream not in gzip_types). Floods ae=gzip: JSON RPS ~1987 off / ~1031 l1 / ~563 l6; page.html ~2123 / ~2295 / ~1469. Affiliates: 0. Evidence: lab-evidence/20-nginx-gzip/.
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20263 min read
Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read