Cold Spare

MEASUREMENT

What backup compression actually buys a home server

Thirteen settings, five kinds of data, 282 MB. The slowest setting spends 360 times longer than the fast one to save an extra 8.0 MB.

Every backup tool asks you this question and none of them help you answer it. Borg wants a --compression value, restic wants a compression mode, ZFS wants a property on the dataset, and the forum answer is always somebody insisting that zstd-19 is obviously right because it compresses more.

It does compress more. The question is how much more, on the data you actually have, and what that costs you in the two places where it hurts: the length of the nightly backup window, and the length of the restore when something has gone wrong. Those are measurable, so here they are measured.

The short version

On a 282 MB corpus assembled to look like what a home server really stores, zstd -3 using all cores compressed the whole thing in 0.15 seconds and saved 118.3 MB. xz -9 took 54.7 seconds and saved 126.4 MB.

That is 360 times the wall clock for 6.8% more saved bytes — and the reason the gap is that lopsided is not that xz is bad. It is that half the corpus is photographs and video, and no setting compresses those at all.

Time to compress the whole 282 MB corpus Time to compress the whole 282 MB corpus 1.0s 10.0s seconds (log scale) gzip -6 3.9s saves 118 MB gzip -9 6.9s saves 118 MB bzip2 -9 14.3s saves 124 MB xz -6 46.0s saves 126 MB xz -9 54.7s saves 126 MB xz -6 -T0 23.1s saves 126 MB zstd -1 0.23s saves 116 MB zstd -3 0.34s saves 118 MB zstd -9 1.1s saves 122 MB zstd -19 37.1s saves 125 MB zstd -19 --long=27 45.1s saves 125 MB zstd -3 -T0 0.15s saves 118 MB zstd -19 -T0 20.4s saves 125 MB
Wall clock to compress all five categories, single stream unless the setting says -T0. Log scale, because the spread is three orders of magnitude. The annotation is how many megabytes each setting saved out of 282.

The corpus

A benchmark on a Linux kernel tarball tells you how a codec handles source code, which is not what is on your NAS. This corpus was assembled to match the mix on a home server, and every part of it is fetched from a stable public URL by a script you can run, so the whole thing is rebuildable rather than merely described.

The corpus: 281.9 MB in five categories, each packed into its own uncompressed tar.
CategorySizeWhat it is
code112.4 MBThe SQLite 3.46.0 source tree: C, headers, test scripts, makefiles
photos110.5 MB25 JPEG photographs, 2–6 MB each, from Wikimedia Commons
video30.7 MBOne 1080p H.264 clip (Big Buck Bunny, CC-BY, Blender Foundation)
db19.3 MBA SQLite database built from the OurAirports open dataset, indexed and vacuumed
docs8.9 MBTen plain-text books from Project Gutenberg

The photo sizes are deliberate: featured images on Wikimedia run to 20 or 30 MB, which no phone produces. Files in the 2–6 MB band are what a real library is full of.

How this was measured

Each category tar is fed to each codec on stdin, output captured to a file, then decompressed back and the restored size asserted against the original, so a setting that silently truncated would fail rather than post a good ratio. Timing is wall clock around the codec process. One run per pair, no median of repeats.

Machine: Apple M4, 10 cores, 16 GB, macOS 26.3.1, internal NVMe. zstd 1.5.7, xz 5.8.3, Apple gzip 475, bzip2 1.0.8. Settings marked -T0 use all ten cores; everything else is pinned to one, so the single-thread rows compare codecs and the -T0 rows show what the hardware adds.

Script: compress-bench.py. Corpus builder: build-corpus.sh. Raw output: compress-results.jsonl.

Ratio, by kind of data

Compression ratio by kind of data Compression ratio by kind of data 0.00x 5.00x 10.00x 15.00x 20.00x times smaller (linear scale) code 9.37x 13.97x docs 2.82x 3.61x db 2.48x 3.65x photos 1.01x 1.01x video 1.00x 1.00x zstd -3 -T0 xz -9
The fastest sensible setting against the slowest one measured. Linear scale — 1.00x means the file came out the same size it went in.
Compression ratio, times smaller. Higher is better.
Settingcodedocsdbphotosvideo
gzip -68.74x2.68x2.52x1.01x1.00x
gzip -98.96x2.69x2.54x1.01x1.00x
bzip2 -910.93x3.66x3.12x1.02x1.00x
xz -613.78x3.61x3.64x1.01x1.00x
xz -913.97x3.61x3.65x1.01x1.00x
xz -6 -T013.67x3.61x3.64x1.01x1.00x
zstd -18.36x2.42x2.27x1.00x1.00x
zstd -39.37x2.82x2.48x1.01x1.00x
zstd -911.53x3.13x2.73x1.01x1.00x
zstd -1913.44x3.58x3.09x1.01x1.00x
zstd -19 --long=2713.69x3.58x3.10x1.01x1.00x
zstd -3 -T09.37x2.82x2.48x1.01x1.00x
zstd -19 -T013.44x3.58x3.09x1.01x1.00x

Three things fall out of that table.

Half your bytes are already compressed, and nothing changes that

Photos measured 1.01x at every single setting; video measured 1.00x at every single setting. JPEG and H.264 have already had the redundancy squeezed out of them, and a general-purpose compressor cannot find any more. xz -9 spent 30 seconds on the photo tar to save one megabyte out of 110.

On a real home server the imbalance is worse than in this corpus. Here photos and video are half the bytes; on an actual family NAS they are usually 80 or 90% of them, with a few gigabytes of documents and databases hiding underneath. Whatever ratio a compression setting achieves on the compressible remainder gets diluted by that dead weight before it reaches your disk usage.

On the data that does compress, the expensive settings win by less than advertised

Source code went from 9.37x at zstd -3 to 13.97x at xz -9 — a real improvement, roughly 49% better. But zstd -9 reached 11.53x at 209 MB/s, which is 24 times the throughput of xz -9 for two thirds of its gain. The database and the text behaved similarly: 2.48x to 3.65x for the database, 2.82x to 3.61x for the books.

The one genuine surprise: bzip2 -9 beat every other setting on plain text, at 3.66x against xz -9's 3.61x and zstd -19's 3.58x. It is the only category where the 1996 algorithm wins, and it wins by a margin too small to matter, which is a reasonable summary of bzip2's position generally.

Threads are free ratio, levels are not

zstd -3 compressed the source tree at 860 MB/s on one core and 3,078 MB/s on ten, at exactly the same 9.37x. Running zstd -19 with -T0 cut the whole-corpus time from 37.1 seconds to 20.4 while holding 1.80x. xz -6 -T0 did the same for xz, 46.0 seconds down to 23.1.

If you are choosing between a higher level and more threads, threads cost you nothing in ratio. Levels cost you throughput on a curve that goes vertical somewhere around zstd -10.

The number nobody checks: how fast does it come back?

Backup speed is the figure everyone compares. Restore speed is the one you find out about on the worst day of the year, and the spread there is wider.

Decompression throughput, MB/s of original data. Measured on the code corpus.
SettingDecompressTime to read back 2 TB
gzip -61,854 MB/s18 min
gzip -91,925 MB/s17 min
bzip2 -9120 MB/s4.6 h
xz -6292 MB/s1.9 h
xz -9290 MB/s1.9 h
xz -6 -T0604 MB/s55 min
zstd -12,300 MB/s14 min
zstd -32,169 MB/s15 min
zstd -92,596 MB/s13 min
zstd -192,524 MB/s13 min
zstd -19 --long=272,257 MB/s15 min
zstd -3 -T02,247 MB/s15 min
zstd -19 -T02,529 MB/s13 min

bzip2 reads back at 119.6 MB/s on the code corpus and 32.4 MB/s on the photos. xz manages 290 MB/s. zstd decompresses at between 2,168 and 2,596 MB/s regardless of the level it was compressed at — a property of the format, not of the setting, and the reason it has taken over.

Scale that to a real restore. Reading 2 TB back through bzip2's 58 MB/s on mixed data is about ten hours of pure decompression before anything else in the restore has happened. Through zstd it is under an hour. That difference does not show up in any ratio table, and it is the one you will care about.

The caveat that matters here: on a NAS the disk or the network is usually slower than any of this. Over a gigabit link you get about 110 MB/s, so every setting above that is bottlenecked elsewhere and the ratio is all that is left to argue about. Below it — bzip2 on mixed data, xz on photos — the codec is genuinely the thing holding you up.

What to set

The backup window calculator runs these measured figures against a data mix and a time budget you enter, if you would rather see it applied to your own numbers than reason from the table.

What this does not tell you

Scripts, corpus builder and the full JSON output are on the raw data page. If your own numbers come out differently, send them — a result that contradicts this one is worth more than another that agrees.