HotShard
Probabilistic data structure

Bloom Filter

A tiny bit array that says 'definitely not here', so you skip the expensive lookup.

You need to check whether something is in a huge set, and every check costs a disk read or a network call. Most of the time the answer is no. A Bloom filter is a small in-memory structure, often just kilobytes, that answers most of those no's instantly, so the expensive lookup never happens. It can be wrong in one direction only, and you control how often.

~6 min read

Start here: the problem it solves#

TL;DRthe 30-second version
  • A Bloom filter is a bit array plus k hash functions. To add a key, set the k bits it hashes to. To query a key, check those k bits: any 0 means definitely absent, all 1s means probably present.
  • It can be wrong in one direction only. A false positive (says 'probably present' for a key never added) is possible. A false negative (says 'absent' for a key that was added) is impossible, because bits are only ever set, never cleared.
  • About 10 bits per key buys a ~1% false-positive rate. A hash set needs hundreds of bits per key. In return, the filter can't list its contents, count, or delete.
  • The sizing rule: optimal k = (m/n) Γ— ln 2, which sets about half the bits. Each extra bit per key multiplies the false-positive rate by ~0.6.

Imagine a web crawler that has visited 100 million URLs. Every time it finds a new link, it asks: have I already crawled this? A hash set of all 100 million URLs would take roughly 8 to 10 GB of RAM. And the crawler does this check billions of times.

This is the general shape. A cheap membership test guards an expensive operation: a disk seek, a round trip to a database, a re-download. Most of the time the answer is 'no, not present'. If you could answer the 'no' cases in memory, you'd skip the expensive lookup almost every time.

The key observation is that the crawler doesn't need a perfect answer. It can tolerate revisiting a page it already crawled. That's a false positive: the filter wrongly said 'maybe seen'. It cannot tolerate missing a new page because the filter wrongly said 'already seen'. That's a false negative. One direction of error is fine; the other is not. A Bloom filter gives you exactly that asymmetry.

The trade-offAt about 10 bits per URL, the filter for 100 million URLs is roughly 125 MB instead of 8 GB. That's a ~64x saving. The price is a small, tunable false-positive rate, and a false positive here just means one wasted lookup, not a wrong answer.

How it works: set k bits, check k bits#

A Bloom filter is a large array of bits, all starting at 0. Alongside it you have k hash functions. A hash function takes any input, like a URL, and always returns the same position in the array. Each of the k functions uses different math, so one input maps to k different positions. The hashes only need to be fast and spread inputs evenly; they don't need to be cryptographic. k is small, typically 3 to 10.

To add a key, run it through all k hash functions and set those k bits to 1. We'll use a 12-bit array so every step is visible.

keyβ€œalice.com”
hash Γ—3 β†’
k = 3 hashesh1, h2, h3
ADD β†’
set bits 2, 5, 9to 1
ADD: hash the key with k functions, set those k bits to 1

highlighted = the 3 bits alice set

bits00011203041506070819010011
The bit array after adding β€œalice.com”
  1. ADD 'alice.com' with k=3: the hash functions return positions [2, 5, 9]
  2. Set bit 2, bit 5, and bit 9 to 1 (the highlighted bits above)
  3. ADD 'bob.com': the hash functions return positions [7, 2, 10]
  4. Set bit 7 to 1 (new). Bit 2 is already 1, so no change. Set bit 10 to 1 (new)

Bit 2 was already set by alice.com, and bob.com maps to it too. Two keys sharing a bit is a collision. Collisions are harmless during adds. They matter during queries, where they are the source of false positives.

To query a key, run it through the same k hash functions and look at those positions.

  • If any of the k bits is 0, the key was definitely never added. Adding it would have set all k bits. This answer is always correct.
  • If all k bits are 1, the key was possibly added. Those bits might have been set by other keys. This is a 'maybe'. It could be a genuine match or a false positive.
QUERYBits checkedVerdict
carol2 = 1 βœ“, 4 = 0 βœ—Definitely not present. One 0 ends it; skip the disk read
mallory2 = 1 βœ“, 5 = 1 βœ“, 9 = 1 βœ“Probably present, but it could be a false positive

The no-false-negatives guarantee follows from one rule: bits are set, never cleared. If you added a key, its k bits are 1 and stay 1, so a later query for it always sees all 1s. The filter can only be wrong the other way: saying 'possibly present' for a key that was never added.

PredictA query can stop the moment it finds a 0 bit. So for a key that is genuinely absent, roughly how many of the k bits does it inspect on average?

Hint: About half the bits in a well-tuned filter are 1. What's the chance the first probed bit is already 0?

Usually one or two. At the optimal operating point about half the bits are 0, so there's roughly a 50% chance the very first probe lands on a 0 and the query returns 'absent' at once. On average a true-negative lookup inspects under two bits. That's why a Bloom check is essentially free compared to the disk read it prevents. Only a false positive, and every true positive, has to inspect all k bits.

One practical detail. You don't compute k separate hashes per key. Implementations compute two hashes, h1 and h2, and derive the i-th position as h1 + i Γ— h2 (mod m). Kirsch and Mitzenmacher showed this costs no meaningful accuracy. That's why RocksDB and Cassandra can use one fast hash like MurmurHash for the whole thing.

Sizing it: bits per key and the false-positive rate#

As you add keys, more bits flip to 1. Eventually a key that was never added hashes to k positions that are all already 1, and the filter says 'possibly present'. That's a false positive. The fuller the array, the more often it happens.

  • At 10% fill (1 in 10 bits set): false positives are rare. Most queries find a 0 and correctly return 'absent'.
  • At 50% fill: the optimal operating point. A good balance of memory and accuracy.
  • At 90% fill: almost every query returns 'possibly present' whether or not the key was added. The filter is nearly useless.
The false-positive rate formulaThe false-positive rate is about (fill ratio)^k: the chance that all k checked bits are already 1. At 50% fill with k = 7, that's 0.5^7 = 0.78%. With k = 3, it's 0.5^3 = 12.5%. More hashes lower the rate at the same fill, but each insert also sets more bits, so the array fills faster. Past a point, more hashes make it worse.

Two numbers control everything. m/n is the bits per key: total bits m divided by expected keys n. k is the number of hash functions. The optimal k is (m/n) Γ— ln 2, about 0.69 Γ— bits per key. At that k, half the bits are set on average, and the false-positive rate is as low as it can be for the memory you have.

Bits/key (m/n)Optimal kFalse-positive rate
64~5%
86~2%
107~1% (the common default)
1410~0.1%
2014~0.007%
Rule of thumbEach extra bit per key multiplies the false-positive rate by about 0.6, so every ~1.4 extra bits halves it. 10 bits per key buys ~1%; use that unless you have a specific target. Bits per key doesn't depend on how many keys you store. A filter for a billion keys at 1% needs the same 10 bits each as one for a thousand keys.

Time is the easy part. Add and query are both O(k): a fixed handful of hashes and bit accesses, no matter how many keys the filter holds. Space is O(n) bits, with the constant set by your target false-positive rate.

Trade-offs: what you give up for the space
  • Space vs accuracy is the central dial. Fewer bits per key saves memory but raises the false-positive rate, and the ~0.6 multiplier per bit is fixed. You pick a point on that curve; you can't beat it.
  • 'Present' is never certain. If a wrong 'yes' is catastrophic rather than a wasted lookup, a Bloom filter alone is the wrong tool.
  • No deletes in standard form. Bits are shared between keys, so clearing one key's bits would create false negatives for others. A counting Bloom filter replaces each bit with a small counter (typically 4 bits, so ~4x the memory) and decrements to delete. The common production alternative is to rebuild the filter from the source of truth.
  • Can't enumerate or count. It answers 'is x a member?' and nothing else. For approximate distinct counts, use a HyperLogLog.
  • Sized up front. You must estimate n to pick m. Overshoot and you waste memory; undershoot and the false-positive rate silently degrades. A scalable Bloom filter chains filters of growing size when you can't know n in advance, at the cost of slower queries.
Bloom filter vs hash set vs cuckoo filter
Bloom filterHash setCuckoo filter
Memory per key~10 bits (configurable)~100–400 bits (stores full keys)~7–12 bits at low FPP
'Absent' answerCertain, no false negativesCertainCertain, no false negatives
'Present' answerProbable, tunable FPPCertainProbable, tunable FPP
DeletesNo (counting variant adds it)YesYes, natively
List / countNoYesNo
Lookup workO(k) hashes, scatteredO(1) averageO(1), ≀2 cache lines
CapacityDegrades gracefully when overfullGrows freelyInserts can fail when full
Best forCheap pre-check before an expensive lookupExact membership, small/medium setLike Bloom but needs deletes / lower FPP
Where Bloom filters run in the wild
  • LSM-tree storage engines. RocksDB, LevelDB, Cassandra, HBase, and ScyllaDB attach a Bloom filter to each on-disk SSTable. A point lookup checks the filter first; if it says 'not here', the whole file is skipped with zero disk I/O. This is the single most important use of Bloom filters in databases. RocksDB uses blocked filters, where all k probes hit one cache line, trading a slightly higher false-positive rate for speed.
  • Google Bigtable. The original paper describes optional per-SSTable Bloom filters to avoid disk seeks for rows that don't exist.
  • CDN caches. Akamai uses Bloom filters to detect 'one-hit wonders', objects requested exactly once. Only cache an object the second time it's seen, so the cache isn't polluted by URLs that never come back.
  • Chrome Safe Browsing (historically). Early versions shipped a Bloom filter of malicious URL prefixes so most safe URLs were cleared locally, with no server round trip.
  • Bitcoin SPV clients (BIP 37). Lightweight wallets sent a Bloom filter of their addresses to full nodes so the node returned only relevant transactions. Later deprecated over privacy and DoS concerns.
Failure modes: saturation and silent degradation

Bloom filters fail quietly. They never throw an error or return a wrong 'absent'. They just get less useful as the false-positive rate climbs, and nothing tells you.

  • Saturation. Insert far more than the planned n and the array fills past 50%. Past ~80–90% fill almost every query returns 'probably present' and the filter saves nothing. Track the actual fill ratio in production, not just the insert count. Many systems sidestep this entirely: an SSTable's filter is built once at flush time for a known key count.
  • Over-hashing. A k much larger than (m/n) Γ— ln 2 sets too many bits per insert and saturates the array faster. More hashes is not always better.
  • Bad or correlated hashes. Keys cluster into fewer bits than expected, and the real false-positive rate lands above what the formula predicts.
  • Mistaken deletes. Clearing bits in a standard filter, or decrementing a counter for a key that was never added, creates false negatives. This is the one way to make a Bloom filter lie about absence.
In an interview
  • Lead with the use case. It sits in front of something expensive, a disk read or a network call, and cheaply rules out keys that are definitely absent.
  • Then the mechanism in two sentences. k hash functions map a key to k bit positions. Add sets them; query checks them. One 0 means absent, all 1s means maybe.
  • State the guarantee and why it holds: no false negatives, because bits are never cleared.
  • Know the sizing rule: ~10 bits per key, k β‰ˆ 0.69 Γ— bits per key, ~1% false positives at 50% fill.
  • Name the limitation, no deletes, and the counting Bloom filter as the fix.
  • If pressed on alternatives, contrast a cuckoo filter (adds deletes and lower false-positive rates) and a HyperLogLog (a different job: approximate distinct count, not membership).
Do more hash functions always reduce false positives?

No, there is an optimum. More hashes mean more bits checked on a query, which is good, but also more bits set on every insert, which fills the array faster. The sweet spot is k = (m/n) Γ— ln 2, where about half the bits are set. Above it the array saturates and the false-positive rate climbs again.

Can a Bloom filter ever produce a false negative?

Not a standard one. If a key was added, all k of its bits were set and bits are never cleared, so a later query always sees all 1s. The only way to get one is to break the rules: clearing bits in a plain filter, or decrementing a counter for a key that was never inserted.

Can I delete from a Bloom filter?

Not from a standard one. Bits are shared between keys, so clearing one key's bits would corrupt others. Use a counting Bloom filter (counters instead of bits, ~4x memory) or a cuckoo filter. In production, the usual answer is to rebuild the filter from the source of truth.

Is the false-positive rate fixed forever?

No. It rises as you insert more keys. The advertised rate assumes you stay at or below the planned capacity n. Overfill the filter and the rate degrades silently, with no error to warn you. Monitor the fill ratio, not just the count.

References & further reading
References

Feedback on this topic β†’