On raw bytes, Avro is 2.74× smaller than JSON — the number people quote to justify switching. But JSON's bulk is repeated field names, exactly what gzip removes. Compress the batch and the advantage collapses to 1.43×: the size win was mostly compressible overhead.
Encode a batch of 10,000 records four ways and the binary formats look like a runaway win: Avro is 2.74× smaller than JSON on the raw bytes. But most of JSON's bulk is the same field names and punctuation repeated 10,000 times — which is precisely the redundancy gzip was built to crush. So gzip each blob and watch the advantage largely collapse: after compression Avro is only 1.43× smaller than JSON, because gzip already removed the very overhead that made the binary formats look good. You will predict the raw gap, then predict what survives compression — and find the size argument for binary mostly evaporates.
Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 5 — Encoding and Evolution, §"Binary encodings" (and §"The Merits of Schemas"):
a compact encoding is not the only reason to use these formats.
The book lists schema formats' merits beyond size. This exercise pressure-tests the size merit itself: measure the raw gap, then compress and see how little of it is left.
The surprise is that a "2-3× smaller" number is mostly compressible overhead. These help; each line notes where it shows up.
"userName" 10,000 times; Protobuf/Avro keep the names in a schema instead. This is the redundancy that inflates the raw JSON and that gzip later removes.uv, with msgpack, fastavro, and Protocol Buffers (grpcio-tools compiling the .proto at runtime + the protobuf runtime) — no Docker; gzip is stdlib. A batch of 10,000 user records {id, userName: "user{i}", favoriteNumber, interests[2]}, generated from a fixed formula (no randomness), gzipped at level 9.
The same 10,000-record batch, measured twice:
gzip.compress(blob, 9) and count again. gzip finds and collapses repeated byte sequences — and JSON's biggest feature is repetition. So the format that looked worst raw has the most to gain from compression, and the four sizes bunch back together.We compare each format to JSON both raw and gzipped. The whole exercise is one script, code/compression_gap.py.
cd study/ddia/ch05/code
uv run --with msgpack --with fastavro --with grpcio-tools --with protobuf \
python3 compression_gap.py
Do not read the output yet — make each prediction first.
Predict. Encode 10,000 records. JSON as newline-delimited objects, and Avro as concatenated schemaless values. How much smaller is the raw Avro blob than the raw JSON blob — 1.5×? 2×? 3×? Write down a number.
Do. Run the script.
Raw, the binary formats win big: Avro is 0.36× JSON's size — 2.74× smaller. That is the number people quote when they say "switch to Protobuf and cut your bytes in half." Look at the raw column and binary looks like an easy win.
Predict. Now cover the gzip columns. gzip compresses all four blobs. Does the 2.74× Avro-vs-JSON gap hold after compression, widen, or shrink?
The gap collapses from 2.74× to 1.43×. gzip crushes the 924 KB JSON blob by 89%, down to 97 KB, because those 10,000 repeats of "userName", "favoriteNumber", "interests", and the braces and quotes are exactly the redundancy DEFLATE eats. Avro started with far less to remove (337 KB → 68 KB), so the two end up close. The raw 2.74× advantage was mostly compressible overhead — it never survived the wire once you turned compression on.
Every format stores the same values; they differ in how much self-description they add. JSON adds the most: in a 10,000-record batch the string "userName" appears 10,000 times, "favoriteNumber" 10,000 times, plus braces, quotes, colons, and commas throughout. That is why the raw JSON blob is 924 KB while Avro — which carries the field names once, in a schema the reader already has — is 337 KB. On the raw bytes, dropping repeated self-description is worth 2.74×.
But "repeated byte sequences" is the exact thing gzip's LZ77 stage is designed to eliminate: the first "favoriteNumber": costs its full length; every later copy becomes a short back-reference pointing at the first. So the format with the most repetition has the most to gain from compression. gzip takes 89% off JSON but far less off Avro, whose bytes were already close to pure payload with little for the compressor to find. The rankings that looked so different raw converge: Avro's 2.74× lead shrinks to 1.43×. Compression, in effect, gives JSON a schema of its own — a dynamically built dictionary of the repeated strings — reclaiming most of what a static schema bought Protobuf and Avro.
N = 1 and re-run. Predict what gzip does to a ~90-byte JSON object (hint: gzip has a fixed header and needs repetition to win) — and watch the raw ranking survive because there is nothing to compress.zstandard at a high level, or brotli, in place of gzip. Predict whether a stronger compressor closes the JSON↔Avro gap more (better at the repeats) or reveals a residual binary edge.userName: "user{i}" with a random 40-char token per record so the field values, not just the names, dominate. Predict how the raw ratio and the gzipped ratio both move as the payload stops being repetitive.Sources: DDIA 2e, Ch. 5, §"Binary encodings", §"The Merits of Schemas" · RFC 1951 (DEFLATE), RFC 1952 (gzip) · the MessagePack, Protobuf, and Avro encoding specs.