Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why → how

Why Bloom filters exist

A data structure that answers "have I seen this?" with "definitely no" or "maybe" — and saves enormous amounts of work by being wrong on purpose.

Data intro Apr 29, 2026 · updated Aug 25, 2026 · 11 min read

On this page

The picture version

Five pictures for a reader who has never heard of a Bloom filter, following one question: is the word you just typed in the dictionary?

1 · The problem

You need the answer without holding the dictionary.

the quick brown foxx one question per keystroke: is this string in the dictionary? every check goes here slow the dictionary on disk, over the network, or just huge A membership check you have to answer without holding the set. a real hash set of a billion keys can run into many tens of gigabytes — far too big to put one in front of every shard, every file, every cache
Keeping the dictionary and looking is the honest answer, and it is exactly what you can’t afford when the check has to be instant. The whole design starts from refusing to hold the set.

2 · The asymmetry

Being wrong in one direction only turns out to be free.

the filter answers “definitely no” “maybe” never wrong stop here — the word is not in the dictionary, guaranteed sometimes wrong fall through to the slow path you were going to take anyway A wrong “no” would be a bug. A wrong “maybe” is a missed optimisation.
That asymmetry is the currency the whole structure spends. Because a “maybe” only costs you the lookup you were already going to do, the filter can be allowed to be wrong — but only ever in that one direction.

3 · The mechanism

Each word flips four switches. Shared switches.

insert “banana” 3 17 42 88 flip all four on insert “orange” 3 22 71 99 3 was already on — it stays on query “bananna” 3 17 50 99 DEFINITELY NO one switch off is proof query “melon” 3 22 42 88 MAYBE nobody inserted it — a false positive banana and orange between them happened to flip every switch melon needed. A “definitely no” can never be wrong: an inserted word would have all four of its switches on.
The switches are shared between words, which is where both properties come from at once: it is why the filter fits in so little memory, why a false positive is possible at all, and why you can never flip a switch back off — that would erase evidence of some other word too.

4 · The ingredient people forget

A filter nobody sized quietly stops filtering.

the space cost is a formula, not a guess m ≈ −n · ln(p) / (ln 2)² 1% false positives 9.6 bits per key 0.1% false positives 14.4 bits per key so a billion keys at 1% is about 1.2 GB — against many tens of GB for a real hash set and this is the failure that actually bites sized for 200,000 words now holding 2 million plenty of switches off ~63% lit the filter has quietly stopped filtering, and nothing in the code changed
When kn/m ≈ 1 the expected fraction of bits set is 1 − 1/e ≈ 63%, so absent words find all their switches already on far more often and the filter stops being worth querying. Sizing is not a tuning detail; it is the ingredient that makes the structure work at all.

5 · Keep this card

The whole thing on one index card.

Bloom filter = a bit array + k hash functions + a sizing rule tied to how many keys go in ∴ “no” is exact; “maybe” is the price wrong in one direction only — and that restriction is the whole currency
Picture to keep: a long row of light switches, all off — every word you add flips four of them, and to check a word you look at its four. Where it breaks: you can never flip one back off, because the switches are shared. That single restriction is where “no deletes” comes from.

Why it exists

Type a word into any text box and a red squiggle appears under it before you’ve hit the next key. Behind that squiggle is a question asked once per word you type: is this string in the dictionary? The honest way to answer is to keep the dictionary and look. The problem is that “keep the dictionary” is exactly what you can’t afford when the check has to be instant and the dictionary lives somewhere slow — on disk, in another datacenter, behind an API. That’s the running example for this post: a membership check you have to answer without holding the set.

Burton Howard Bloom’s 1970 paper (Space/Time Trade-offs in Hash Coding with Allowable Errors, CACM 13(7)) works through almost exactly this shape of problem. Its running application is automatic hyphenation: most words can be hyphenated by simple rules, a minority need a dictionary lookup, and that lookup — a disk access, in 1970 — is what you’re trying to avoid paying.

His move was to notice an asymmetry. If the structure could quickly say “don’t bother, this is definitely not in the set,” and were allowed to occasionally say “maybe” when the truth is “no,” nothing breaks. A “maybe” just means you fall through to the slow path you were going to take anyway. A wrong “no” would be a correctness bug; a wrong “maybe” is a missed optimization. Being willing to be wrong in one direction only turns out to be a currency you can spend for enormous amounts of memory.

Why it matters now

Memory is cheaper than it was in 1970, but the gaps between RAM, local SSD, the network, and cold object storage are still orders of magnitude each. Anywhere there’s a steep cliff between two storage tiers, a Bloom filter wants to sit at the top of it. Three concrete places: LSM-tree storage engines (RocksDB, Cassandra, LevelDB, HBase) commonly attach a filter to each on-disk sorted file — an SSTable — so a read can skip files that definitely don’t hold the key; it’s configurable rather than always on (see lsm-trees). Google’s 2006 Bigtable paper describes exactly this refinement, motivated by cutting disk seeks for reads of rows that don’t exist.

And Bitcoin’s SPV wallets used them via BIP 37 to ask full nodes for transactions matching data derived from the wallet’s own keys and addresses, without sending that data in the clear.

That last one is also the cautionary tale. In practice the parameters wallets chose leaked substantially more than intended — published analysis of BIP 37 found that wallets holding only a small number of addresses risked exposing nearly all of them to the node they queried. Bloom filters leak sideways, and the leak is not visible in the false-positive formula.

The short answer

Bloom filter = bit array + k hash functions

Picture to keep: a long row of light switches, all off. Every word you add flips four switches determined by its spelling. To check a word, look at its four switches — if any is off, that word was definitely never added; if all four are on, someone was here, though maybe not the word you’re asking about. Like a row of switches, except you can never flip one back off: switches are shared between words, so turning one off would erase evidence of some other word too. That one restriction is where “no deletes” comes from.

To insert x, you hash it k different ways, take each hash mod the array length, and flip those k bits to 1. To query x, you hash it the same k ways and check those bits. If any is 0, x is definitely not in the set. If all are 1, x is probably in the set — or other insertions collectively flipped exactly those bits. There’s no remove (in the basic version), and no enumerate. You traded both for size.

How it works

Build it from the constraint, one failure at a time. The set is an English dictionary; the queries are words a person just typed.

Naive attempt: store the words. A hash set of a few hundred thousand words is fine. A hash set of a billion keys — which is the case people actually build these for — can run into many tens of gigabytes once you count string bytes, pointers, and load-factor slack. Why it breaks: you wanted this to be small enough to put one in front of every shard, every file, every cache. It isn’t.

Fix: stop storing the keys; set one bit per key instead. Hash each word, set the bit at hash(word) mod m. Now the structure costs m bits total, regardless of key length. A “0” at your word’s bit is a rock-solid “definitely not present.” Why it breaks: with one hash into a bit array, collisions are brutal. Once the array is even modestly occupied, a huge fraction of absent words land on a bit some other word already set, and the filter degenerates into answering “maybe” to everything — which is the same as having no filter.

Fix: use k bits per key instead of one. Require all k bits to be set before you’ll say “maybe.” A false positive now needs k independent coincidences instead of one, so the error rate falls roughly geometrically while the space cost grows only linearly. Concretely, with k = 4:

Why it breaks: k independent hash functions is k times the hashing work on the hot path. Fix: derive them from two. Take two good base hashes h1, h2 — often by splitting a single fast 128-bit non-cryptographic hash (MurmurHash3’s 128-bit variant, or XXH3) in half — and compute index i as (h1(x) + i · h2(x)) mod m for i = 0..k−1. That’s Kirsch & Mitzenmacher’s “less hashing, same performance” result; the split-one-128-bit-hash trick is the practitioner’s shortcut on top of it.

Why it breaks: you still have to choose m and k, and guessing wrong is how these fail in production. Fix: derive them. After inserting n items into m bits with k hashes, the probability a random query returns a false positive is approximately (1 − e^(−kn/m))^k. Two consequences worth remembering:

Those constants aren’t magic numbers to memorize — they fall straight out of minimizing the equation above, and Mitzenmacher & Upfal’s Probability and Computing (linked below) walks the derivation if you want to watch 9.6 appear.

The seams — places the abstraction leaks:

Back to the red squiggle. It appears because the filter said “definitely not in the dictionary” — the one answer a Bloom filter is never wrong about — and it fails to appear on a rare typo that happened to light up all four of its bits. You started with Bloom filter = bit array + k hash functions. What did this post add? — + a sizing rule tied to how many keys you'll insert. The array and the hashes give you the shape; m ≈ −n·ln(p)/(ln 2)² is what makes it actually work, and it’s the ingredient people forget. A Bloom filter nobody sized is a Bloom filter that will quietly start answering “maybe” far too often to be worth asking.

Going deeper