Choose general-purpose compression
A general-purpose codec such as ZSTD or LZ4 finds repeated byte patterns in any data, after whatever encoding was already applied. It trades CPU for fewer bytes, and that trade only pays when moving bytes is slower than decoding them. Should you add one, and how should it be configured?
You will end with whether to compress at all, and if so the block size, level, dictionary policy, codec and safeguards.
Work through these in order: the first can make the rest unnecessary.
Work through it
Decide whether to compress at all
Compression pays only if decoding is fast enough relative to the pipe: decode speed ÷ pipe speed > ratio ÷ (ratio − 1). After good lightweight encoding the extra ratio is typically 1.2–1.5×; at 1.35, decoding must run nearly four times faster than the pipe. Over object storage at about 80 MB/s per connection, easily true. On local NVMe at 3–7 GB/s, it needs 12–28 GB/s of decoding: several cores doing nothing else. If it does not pay: skip the pass. That is correct more often than people think.
Choose the block size from the access pattern
Scans: large blocks, 256 KB to 1 MB. Point lookups: small blocks, 4–16 KB. This matters more than the codec: a 100-byte lookup from a 256 KB block reads about 2,600 times the bytes it needs, and one corrupted bit destroys every record in the block.
Choose the level from reads per write
The level only controls how hard the encoder searches. Read many times: a high level, applied when data is compacted, not at ingest. Read rarely: level 1–3.
Small, similar records: a trained dictionary
If blocks are small and records share structure: train a dictionary on sample data, which brings back ratio a small block cannot find on its own. Treat it as a permanent, versioned artifact, not a setting: record its ID in each block and keep every old version forever, because a missing dictionary means unreadable data.
Choose the codec last
It is nearly always ZSTD; LZ4 if decompression speed is the limit; deflate only if a protocol requires it.
Checksum every block
Always. It is nearly free, and it detects the corruption that compression magnifies: a bad block is reported and repaired from a replica instead of returning noise.
Compress, then encrypt; never the reverse
Good encryption output looks random, so it cannot be compressed: if compressing after encryption achieves anything, the cipher is broken. In the right order a small leak remains: the length of the encrypted data reveals the compressed length. For data at rest this rarely matters, except with per-column encryption plus per-column compression, where the file footer exposes each column's compressed size.