Skip to content
About
Main content

How big is JSON really, after compression?

Measure your machineNeeds Python with fastavro, zstandard

Problem

A such as JSON repeats every field name in every record and writes numbers as decimal text; a schema-based binary format such as writes neither the names nor the text, so its raw size is several times smaller. A general-purpose compressor removes much of that difference, because repeated field names are exactly what it is good at. What remains after is the information the data really carries, and that is the same for both.

Reproduce "JSON is 7× bigger than Avro raw, but only ~1.3–2× after compression". The script encodes 1,000 parcel-scan records both ways, compresses both with level 9, then repeats with sequential parcel IDs.

The question. How much bigger is JSON than Avro before compression, and after zstd?


Predict

After zstd, how much bigger is JSON than Avro for the same records (random IDs)?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    how-big-is-json-really-after.py45 linesDownload
    View the code
    how-big-is-json-really-after.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["fastavro", "zstandard"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """How big is JSON really, after compression?
    
    The same 1,000 parcel-scan records as JSON lines and as Avro (no field names,
    binary numbers), raw and after Zstd level 9; then with sequential parcel ids.
    """
    import argparse, io, json, random
    import zstandard as zstd
    from fastavro import schemaless_writer, parse_schema
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--records", type=int, default=1000, help="Records.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    schema = parse_schema({"type": "record", "name": "Scan", "fields": [
        {"name": "parcel_id", "type": "long"}, {"name": "scanner_id", "type": "int"},
        {"name": "event_time", "type": "long"}, {"name": "status", "type": "string"},
        {"name": "weight_grams", "type": "int"}, {"name": "depot", "type": "string"}]})
    z = lambda b: len(zstd.ZstdCompressor(level=9).compress(b))
    rows = []
    for label, seq in (("random parcel ids", False), ("sequential parcel ids", True)):
        random.seed(1)
        recs = [{"parcel_id": (10**8 + i) if seq else random.randrange(10**9), "scanner_id": random.randrange(500),
                 "event_time": 1_700_000_000_000_000 + i * 1000, "status": random.choice(["LOADED", "SCANNED", "DELIVERED"]),
                 "weight_grams": random.randrange(100, 30000), "depot": random.choice(["PUNE-01", "MUMBAI-02", "DELHI-01"])}
                for i in range(args.records)]
        js = "\n".join(json.dumps(r) for r in recs).encode()
        buf = io.BytesIO()
        for r in recs: schemaless_writer(buf, schema, r)
        av = buf.getvalue()
        rows.append({"ids": label, "json_raw": len(js), "avro_raw": len(av), "raw_ratio": len(js) / len(av),
                     "json_zstd": z(js), "avro_zstd": z(av), "zstd_ratio": z(js) / z(av)})
        r = rows[-1]
        print(f"{label:<22} raw: JSON {r['json_raw']:,} B, Avro {r['avro_raw']:,} B ({r['raw_ratio']:.1f}x)   "
              f"zstd: JSON {r['json_zstd']:,} B, Avro {r['avro_zstd']:,} B ({r['zstd_ratio']:.1f}x)")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"sizes": rows}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run how-big-is-json-really-after.py
    uv run how-big-is-json-really-after.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--records1000 Records.

Our run

1,000 records, two encodings, raw and compressed (bytes)

1,000 records, two encodings, raw and compressed (bytes)
IDsJSON rawAvro rawRaw ratioJSON zstdAvro zstdzstd ratio
random parcel ids141K34.8K4.04×15.8K13.5K1.17×
sequential parcel ids141K34K4.14×11K11.9K0.929×
Reference runApple M2 · 8 cores · 8 GB
Output · uv run how-big-is-json-really-after.py
random parcel ids      raw: JSON 140,659 B, Avro 34,817 B (4.0x)   zstd: JSON 15,833 B, Avro 13,475 B (1.2x)
sequential parcel ids  raw: JSON 140,810 B, Avro 33,982 B (4.1x)   zstd: JSON 11,022 B, Avro 11,863 B (0.9x)
macOS 26.6.2 · Python 3.13.3 · fastavro 1.13.1 · zstandard 0.25.0 · run 2026-10-20

What we found

Raw, JSON was 4.04× the size of Avro, less than the quoted 7×: these records have six short fields, so the field names are a smaller share than in wider records. After zstd the gap shrank to 1.17×. With sequential IDs it closed entirely, to 0.929×: the compressor found the sequence in JSON's decimal text slightly more easily than in Avro's variable-length integers.


Read it

The direction held: the raw gap is large and the compressed gap small. The raw ratio depends on how much of each record is field names, and the compressed ratio on what the data itself contains. Comparing formats by raw size overstates the case for binary formats on the wire and on disk once compression is on.

What the binary format still buys after compression is not size but parse cost and a schema: decoding Avro is far cheaper than parsing JSON, and the schema is a contract.


Go further

Try making parcel_id sequential to see compression help more.