Skip to content
About
Main content

Find your own break-even point D/B

Measure your machineNeeds Python with zstandard

Problem

trades CPU for bytes. Reading a compressed file is faster only if decoding it is faster than reading the bytes it saved: with a ratio r, a decoder producing D bytes per second and storage delivering B bytes per second, compression wins only if D / B > r / (r − 1). The right side is close to 1 for any useful ratio, so the rule is roughly "the decoder must outrun the storage". A fast local SSD can outrun a single-core decoder; a network link to almost never can. The compression level changes the write cost a great deal and the decode speed very little.

Check the rule compression wins only if D / B > r / (r − 1) on your machine. Use any 200–500 MB file with typical data (a CSV or JSON log is fine). The script generates a 300 MB JSON log, measures B for the local disk by reading the file with the page cache bypassed, and uses 0.08 GB/s for an object-store connection.

The question. At each level, is single-core decoding faster than reading the saved bytes from a local disk, and from an object store?


Predict

Against this machine's local disk, does single-core zstd decoding beat reading the uncompressed bytes at any level?

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.

    find-your-own-break-even-point.py59 linesDownload
    View the code
    find-your-own-break-even-point.py
    # /// script
    # requires-python = ">=3.10"
    # dependencies = ["zstandard"]
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """Find your own break-even point for compression.
    
    Compression wins on a read only if D / B > r / (r - 1): decode speed D over
    storage bandwidth B must beat a factor set by the ratio r. Zstd levels 1 to
    19 on a JSON log, against the measured local disk and a slow object-store link.
    """
    import argparse, json, os, platform, random, tempfile, time
    import zstandard as zstd
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--mb", type=int, default=300, help="Size of the generated JSON log, MB.")
    p.add_argument("--levels", type=lambda s: [int(v) for v in s.split(",")], default=[1, 3, 10, 19], help="Zstd levels.")
    p.add_argument("--remote-gbps", type=float, default=0.08, help="An object-store connection, GB/s.")
    p.add_argument("--path", default=os.path.join(tempfile.gettempdir(), "breakeven.jsonl"), help="The sample file.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    import atexit, shutil
    atexit.register(lambda: shutil.rmtree(args.path, ignore_errors=True) if os.path.isdir(args.path) else os.path.exists(args.path) and os.remove(args.path))   # leave nothing behind
    random.seed(0)
    if not os.path.exists(args.path) or os.path.getsize(args.path) < args.mb * 1e6 * 0.99:
        with open(args.path, "w") as f:
            i = 0
            while f.tell() < args.mb * 1e6:
                f.write(json.dumps({"ts": 1_700_000_000 + i, "user": f"u{random.randint(1, 50000)}", "event": random.choice(["view", "click", "buy"]),
                                    "path": f"/p/{random.randint(1, 3000)}", "ms": random.randint(1, 900)}) + "\n"); i += 1
    data = open(args.path, "rb").read()
    
    # B: read the file with the page cache bypassed
    fd = os.open(args.path, os.O_RDONLY)
    if platform.system() == "Darwin":
        import fcntl; fcntl.fcntl(fd, 48, 1)                      # F_NOCACHE
    else:
        os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
    t = time.perf_counter(); n = 0
    while (chunk := os.read(fd, 8 << 20)): n += len(chunk)
    disk = n / (time.perf_counter() - t) / 1e9; os.close(fd)
    print(f"local disk read, page cache bypassed: B = {disk:.2f} GB/s; object store: B = {args.remote_gbps} GB/s")
    
    rows = []
    for level in args.levels:
        t0 = time.time(); comp = zstd.ZstdCompressor(level=level).compress(data); tc = time.time() - t0
        t0 = time.time(); zstd.ZstdDecompressor().decompress(comp); td = time.time() - t0
        r = len(data) / len(comp); D = len(data) / td / 1e9; need = r / (r - 1)
        rows.append({"level": level, "ratio": r, "write_s": tc, "decode_gbps": D, "need": need,
                     "local": "wins" if D / disk > need else "loses", "remote": "wins" if D / args.remote_gbps > need else "loses",
                     "d_over_b_local": D / disk})
        print(f"level {level:2d}  ratio {r:4.1f}  write {tc:6.2f}s  decode {D:5.2f} GB/s  D/B local {D / disk:5.2f} "
              f"need > {need:4.2f} -> local {rows[-1]['local']}, object store {rows[-1]['remote']}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"levels": rows, "summary": [{"disk_gbps": disk}]}}, 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 find-your-own-break-even-point.py
    uv run find-your-own-break-even-point.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--mb300 Size of the generated JSON log, MB.
--levels1, 3, 10, 19 Zstd levels.
--remote-gbps0.08 An object-store connection, GB/s.
--pathos.path.join(tempfile.gettempdir(), 'breakeven.jsonl') The sample file.

Our run

Write time by levelApple M2 · 8 cores · 8 GB
Data table
Write time by level
zstd levelCompress time (s)
10.513
30.521
104.34
19137
macOS 26.6.2 · Python 3.13.3 · zstandard 0.25.0 · run 2026-10-20

The break-even rule at four levels

The break-even rule at four levels
LevelRatioWriteDecode (GB/s)Needed D/BD/B localLocal diskObject store
15.85×513 ms1.421.210.182loseswins
36.04×521 ms1.741.20.223loseswins
107.3×4.34 s1.941.160.249loseswins
198.85×137 s1.761.130.225loseswins
Output · uv run find-your-own-break-even-point.py
local disk read, page cache bypassed: B = 7.81 GB/s; object store: B = 0.08 GB/s
level  1  ratio  5.8  write   0.51s  decode  1.42 GB/s  D/B local  0.18 need > 1.21 -> local loses, object store wins
level  3  ratio  6.0  write   0.52s  decode  1.74 GB/s  D/B local  0.22 need > 1.20 -> local loses, object store wins
level 10  ratio  7.3  write   4.34s  decode  1.94 GB/s  D/B local  0.25 need > 1.16 -> local loses, object store wins
level 19  ratio  8.8  write 136.94s  decode  1.76 GB/s  D/B local  0.22 need > 1.13 -> local loses, object store wins

What we found

Decode speed barely moved with the level, from 1.42 GB/s at level 1 to 1.76 GB/s at level 19, while the write took 513 ms at level 1 and 137 s at level 19. The local disk read at 7.81 GB/s, so D/B was about 0.223 against a needed 1.2: every level lost locally, and every level won against the object store. The local figure is higher than an SSD's rated speed, which suggests the file, written moments before, was still partly served from a cache below the page cache; a slower measured B would only narrow the local loss, not reverse it.


Read it

Compression is a decision about where the bottleneck is. On local the CPU is the bottleneck and compression slows reads unless decoding runs on many cores; on a network or object store the link is the bottleneck and any reasonable codec pays for itself. Higher levels buy ratio with write time, which is worth it for data written once and read many times, and not for hot write paths.