A Bloom filter lets an LSM skip disk reads for keys that aren't there. The space people overestimate: not bytes per key — about 10 bits. A 1.25 MB filter over a million keys gives a 0.8% false-positive rate, matching (1 − e^(−kn/m))^k exactly — and its size is independent of how big the keys are.
An LSM storage engine confirming that a key does not exist may have to check many SSTables — slow. A Bloom filter per SSTable answers "is this key maybe here?" in a few bit tests, so most of those disk reads are skipped. It can say a false "maybe" (a false positive) but never a false "no." The number people get wrong is the space: you don't need bytes per key for a low miss rate — you need about 10 bits. You will guess it takes far more, and watch a 1.25 MB filter give a 0.8% false-positive rate over a million keys.
Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 4 — Storage and Retrieval, §"Bloom filters":
a Bloom filter … provides a fast but approximate way of checking whether a particular key appears in a particular SSTable.
The book describes the structure and that it "takes a bit of extra space." Here you measure exactly how little space, and how the miss rate falls as you spend more of it.
The surprise is a short probability calculation made concrete. These help; each line notes where it shows up.
k of them (here, derived from one SHA-256 digest) to pick k bit positions.n keys, m bits, k hashes, the FP rate is (1 − e^(−kn/m))^k, minimized at k = (m/n) ln 2. This is the curve you measure against.(1 − e^(−kn/m))^k and the optimal k; DDIA §"Bloom filters" (Figure 4-4) is the intuition. The double-hashing trick (#1): search "Kirsch–Mitzenmacher less hashing" — why two hashes can stand in for k of them (what the script does). If only one is new, make it #4 — the formula is the prediction you're testing.
A Bloom filter is m bits, all 0. To add a key, hash it to k positions and set those bits to 1. To test a key, check those same k positions: if any is 0 the key is definitely absent; if all are 1 the key is maybe present — maybe those bits were set by other keys (a false positive). We insert a million keys, then query a million keys we never inserted and count how often the filter wrongly says "maybe," sweeping the bits-per-key budget. The whole exercise is one script, code/bloom_filter.py.
cd study/ddia/ch04/code
python3 bloom_filter.py # ~20 s, standard library only
Do not read the output yet — make each prediction first.
Predict. You want a ~1% false-positive rate over 1,000,000 keys. Roughly how much space do you think that takes — and phrase it per key. (Most people reach for bytes: "a few bytes per key?" — which would be megabytes to tens of MB.)
Do. Run the script. It builds the filter at several bits-per-key budgets, uses the optimal number of hashes for each, and measures the false-positive rate against the formula.
10 bits per key — a 1.25 MB filter — gives 0.83%. Not bytes: bits. And each extra ~2 bits/key roughly halves the miss rate (5.6% → 2.1% → 0.83% → 0.32%). The measured rate sits right on the analytic (1 − e^(−kn/m))^k at every row — this is a formula you can trust, not a lucky run.
Predict. Those keys were short strings. If the keys were 5 KB blobs instead, how much bigger would the filter need to be for the same 0.8%?
No bigger at all. Every key is hashed down to the same handful of bit positions, so the filter's size is set by the key count, never the key length. A filter guarding a million 5 KB records is the same 1.25 MB.
(1 − e^(−kn/m))^k at every budget.A Bloom filter trades a small, tunable error for enormous space savings. Adding n keys with k hashes into m bits, the chance a given bit is still 0 is (1 − 1/m)^(kn) ≈ e^(−kn/m). A false positive needs all k bits checked for an absent key to happen to be 1, which is (1 − e^(−kn/m))^k. Choosing the optimal k = (m/n) ln 2 makes that (1/2)^k = 0.6185^(m/n) — an exponential decay in bits-per-key. That is why the rate falls so fast: 0.6185 to the 10th power is already 0.8%, and every extra couple of bits per key halves it again. Ten bits buys you 1% because the math is exponential, not linear.
The reason it works at all — and the reason an LSM can lean on it — is the one-sided error. A present key set its k bits when it was added, and bits are never unset, so those bits are still 1: the filter never gives a false "no." So a "no" from the filter is the truth ("this SSTable definitely lacks the key, skip the disk read"), and only a "maybe" needs a real lookup. Wrong "maybes" cost one wasted read, at a rate you dial in with bits-per-key; wrong "nos" — which would be catastrophic, dropping real data — cannot happen.
k. Fix k = 3 for every budget instead of the optimal, and re-run. Predict how much worse the FP rate gets at 16 bits/key — the optimum matters most when you have bits to spend.n = 5e6 into the formula with the old m) before measuring — this is what happens when an SSTable has more keys than the filter was sized for.Sources: DDIA 2e, Ch. 4, §"Bloom filters" (Figure 4-4) · the FP formula and optimal k are standard results (see "Bloom filter", Wikipedia) · Kirsch & Mitzenmacher, "Less Hashing, Same Performance."