Hands-on zstd vs gzip vs lz4 on 64MiB payloads (real /usr text, mixed, urandom). Measured compress/decompress MB/s and ratio at default levels. No Docker.
7 min read
“Just enable gzip” and “switch to zstd” are slide-deck sentences. Throughput and ratio depend on payload entropy and level, and pathological repeating text will flatter any codec.
This is a hands-on lab with measured numbers:
os.urandom.Related links:
Lab honesty (1 Oct 2026 IST): Shared Linux lab box (8 cores, kernel 6.12). Python 3.13.5; python-zstandard 0.23.0 (CLI zstd 1.5.7); lz4 4.4.0+dfsg (CLI 1.10.0); system gzip 1.13. No Docker. Affiliates: 0. In-memory microbench — not HTTP Content-Encoding, not a threaded CLI bake-off.
Verdict up front: on realistic text, zstd default beat gzip9 by ~13× compress MB/s at a better ratio (2.39 vs 2.02). lz4 was the compress-speed king (~421 MB/s) with a weaker ratio (1.58). urandom stayed at ratio ≈1.000 for everyone.
| Layer | This lab | Sister labs |
|---|---|---|
| Codec CPU on a buffer | yes | — |
nginx gzip on end-to-end | no | nginx gzip lab |
| Disk write of compressed bytes | no | fsync/fdatasync durable-write lab (packaged alongside; publish separately) |
| Wire capture / TLS | no | openssl TLS lab |
Defaults used:
gzip.compress → level 9 (Python default); also level 6 as a common “fast gzip” knob.zstandard.ZstdCompressor() → level 3; plus 1 and 19 for contrast.lz4.frame.compress → library default frame format.Related links:
First draft used a “random” buffer built from repeating SHA-256 blocks — that still compressed ~80–100×. We reran with true os.urandom and a real multi-file corpus. Evidence notes call that out; quote the rerun.
| Codec | Ratio | Compress MB/s | Decompress MB/s | c p50 |
|---|---|---|---|---|
| gzip9 (default) | 2.015 | 17.8 | 169 | 3.60 s |
| gzip6 | 2.011 | 28.4 | 164 | 2.25 s |
| zstd3 (default) | 2.388 | 227 | 639 | 0.281 s |
| zstd1 | 1.995 | 365 | 624 | 0.175 s |
| zstd19 | 2.765 | 2.5 | 507 | 25.3 s |
| lz4 frame | 1.583 | 421 | 691 | 0.152 s |
zstd3 vs gzip9: ~12.8× compress throughput, better ratio. lz4 wins raw compress speed here but gives up ~0.8 ratio points vs zstd3. zstd19 buys the best ratio (2.77) and loses two orders of magnitude on compress MB/s — offline archival vibes, not request path.
Related links:
| Codec | Ratio | c MB/s | d MB/s |
|---|---|---|---|
| gzip9 | 1.309 | 23.8 | 253 |
| gzip6 | 1.308 | 32.8 | 276 |
| zstd3 | 1.361 | 322 | 1009 |
| zstd1 | 1.301 | 593 | 774 |
| zstd19 | 1.407 | 3.1 | 870 |
| lz4 | 1.218 | 518 | 723 |
Half entropy halves the fairy tale. Ratios collapse toward ~1.2–1.4; zstd/lz4 still crush gzip on compress speed.
| Codec | Ratio | c MB/s | d MB/s |
|---|---|---|---|
| gzip9 | 1.000 | 40.7 | 547 |
| gzip6 | 1.000 | 39.9 | 559 |
| zstd3 | 1.000 | 1181 | 1721 |
| zstd1 | 1.000 | 1406 | 1698 |
| zstd19 | 1.000 | 2.9 | 1473 |
| lz4 | 1.000 | 535 | 578 |
Nobody shrinks pure noise (compressed size ≈ raw; gzip slightly over 100% with framing). Speed here is “how fast can I give up” — zstd still scans fast; zstd19 does not.
Repeating one short line: gzip ~294×, lz4 ~243×, zstd ~10 800× at multi‑GB/s. Useful as an upper bound and a reminder that synthetic compressibility lies. Lead with text_real, not this arm.
/dev/urandom.Related links:
os.urandom.yes \| head.Related links:
All arms used in-process Python:
gzip.compress / gzip.decompresszstandard.ZstdCompressor(level=…) / ZstdDecompressorlz4.frame.compress / lz4.frame.decompressWall time is time.perf_counter around the full buffer op (not streaming chunk APIs). MB/s = 64 MiB / p50_seconds. Ratio = raw_bytes / compressed_bytes. Round-trip equality was asserted every repeat.
text_real was built by walking /usr/share/doc, /usr/share, and /usr/lib/python3.13, skipping ELF/gzip, deduping by a content hash prefix, concatenating until ≥64 MiB, then truncating. That is still “lab box text,” not your production JSON — but it is far closer than a single repeated sentence.
If you re-run on NVMe with the zstd CLI -T0, expect higher absolute MB/s; keep the relative ordering as the portable lesson unless you re-measure.
Related links:
On 64 MiB realistic /usr text, zstd level 3 delivered ratio 2.39 at ~227 MB/s compress and ~639 MB/s decompress — roughly 13× faster compress than gzip9 (2.02 / 17.8 / 169) with a better ratio. lz4 hit ~421 MB/s compress at ratio 1.58. urandom stayed at ≈1.000 ratio for all codecs. Choose the codec for the entropy you actually ship, and keep fairy-tale repeating buffers in the appendix.
Evidence path on the lab box: lab-evidence/33-zstd-vs-gzip-lz4/results/. Affiliates: 0.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5; python-zstandard 0.23.0 (CLI zstd 1.5.7); lz4 4.4.0+dfsg (CLI 1.10.0); gzip 1.13. Payloads 64 MiB. Lead text_real: gzip9 ratio 2.02 c 17.8 MB/s d 169; gzip6 2.01 / 28.4 / 164; zstd3 2.39 / 227 / 639; zstd1 2.00 / 365 / 624; zstd19 2.77 / 2.5 / 507; lz4 1.58 / 421 / 691. urandom ratio ≈1.000 all; zstd3 ~1181 MB/s compress. Pathological text_rep labeled. No Docker. Affiliates: 0. Evidence: lab-evidence/33-zstd-vs-gzip-lz4/.
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on mmap MAP_SHARED write+msync vs pwrite region update: real MB/s with durability labels, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read