Skip to content
About
Main content

Bloom filters

How many bits per key buy which false-positive rate, and what is a filter actually worth?

A answers "is key k in the set?" with either "definitely not" or "probably yes". It is a bit array of m bits (all zero at the start) and k . To insert a key, compute k hash values and set those k bits to 1. To test a key, compute the same k positions: if any bit is 0, the key was never inserted. If all are 1, the key was probably inserted, but those bits might have been set by other keys.

class Bloom:
    def __init__(self, m_bits, k):
        self.m, self.k = m_bits, k
        self.bits = bytearray((m_bits + 7) // 8)
    def _positions(self, key):
        # k positions from two hashes ("double hashing"); illustrative only
        h1, h2 = hash(("a", key)), hash(("b", key))
        return [(h1 + i * h2) % self.m for i in range(self.k)]
    def add(self, key):
        for p in self._positions(key):
            self.bits[p >> 3] |= 1 << (p & 7)
    def maybe_contains(self, key):
        return all(self.bits[p >> 3] & (1 << (p & 7)) for p in self._positions(key))

The derivation

Let there be m bits, n keys and k hash functions. After n insertions, a given bit is still zero with probability (1 − 1/m)^(kn) ≈ e^(−kn/m). So the false-positive rate is

bits in the filter
keys inserted
hash functions per key
Data table
False-positive rate by bits per key (optimal k)
Bits per key m/nFalse-positive rate
238.3%
2.2733.5%
2.5529.4%
2.8325.7%
3.122.6%
3.3819.8%
3.6517.3%
3.9215.2%
4.213.3%
4.4711.6%
4.7510.2%
5.038.94%
5.37.84%
5.586.87%
5.856.02%
6.135.27%
6.44.62%
6.674.05%
6.953.55%
7.223.11%
7.52.72%
7.782.39%
8.052.09%
8.321.83%
8.61.61%
8.881.41%
9.151.23%
9.431.08%
9.70.946%
9.970.829%
10.30.727%
10.50.637%
10.80.558%
11.10.489%
11.30.428%
11.60.375%
11.90.329%
12.20.288%
12.40.252%
12.70.221%
130.194%
13.30.17%
13.60.149%
13.80.13%
14.10.114%
14.40.1%
14.70.088%
14.90.077%
15.20.067%
15.50.059%
15.80.052%
160.045%
16.30.04%
16.60.035%
16.90.031%
17.10.027%
17.40.023%
17.70.021%
17.90.018%
18.20.016%
18.50.014%
18.80.012%
19.10.011%
19.30.009%
19.60.008%
19.90.007%
20.10.006%
20.40.005%
20.70.005%
210.004%
21.30.004%
21.50.003%
21.80.003%
22.10.002%
22.40.002%
22.60.002%
22.90.002%
23.20.001%
23.40.001%
23.70.001%
240.001%

Minimizing this over k gives k* = (m/n) · ln 2. At that point exactly half the bits are set. Half-full is the maximum-entropy state of the array (each bit is as unpredictable as possible), so each bit carries the most information it can. At the optimum, FPR = 2^(−k) = 0.6185^(m/n).

Two numbers follow, and both are worth memorizing:

  • Ten bits per key buys roughly 1% false positives.
  • Every additional 4.8 bits per key divides the false-positive rate by ten. This is the same constant, ln 10 / ln²2, that appeared in the Lookup structures derivation of how to give different LSM levels different filter budgets. It is the same constant because it is the same optimization.

Bloom is 44% above the floor, and that 44% has a meaning

The information-theoretic floor for approximate membership at false-positive rate ε is n · log₂(1/ε) bits. At 1% that is 6.64 bits per key. Bloom spends 9.58. The ratio is exactly 1/ln 2 ≈ 1.44. What you buy with that extra 44% is incremental insertion: you can add keys one at a time, at any moment. The cuckoo, XOR and ribbon filters give some of that 44% back by giving up something. Stated this way, the whole filter family becomes one trade-off rather than three competing designs.

Its space does not depend on key size. Ten bits per key whether the key is an 8-byte integer or a 200-byte URL. That is the underrated property: it lets the filter live in a memory tier much faster than the data it guards.

The real cost in production is cache misses

The k hash probes are k random bit positions in a structure much larger than the CPU cache. That is up to seven cache misses per query, around 500 ns: a sizeable fraction of the NVMe read the filter was supposed to avoid.

Blocked (split-block) Bloom filters fix this. A first hash chooses one cache-line-sized block (64 bytes), and all k bits go inside that block. This turns k misses into one. It follows the Batch-size law: confine the work to one transfer unit. The price is a slightly worse false-positive rate, because keys spread unevenly across blocks (the balls-in-bins variance from earlier articles returns), so you need roughly 10 to 20% more bits for the same rate. Everyone takes this trade now. Parquet's filter specification and Impala's runtime filters use it.

What a filter is actually worth

A filter converts a random I/O into a memory probe with probability 1 − ε. Its value is therefore proportional to the ratio between the tier it guards and the tier it lives in:

Guarded tierRatio to RAM probeValue of a filter
Spinning disk~10⁵free money
NVMe ~10³still pays
Another in-memory table~1pure overhead

This is why the same structure is obviously correct in one tier and obviously wasteful one tier up.

Correct the folklore

"Bloom filters make lookups fast" is backwards. They make negative lookups fast, and they do nothing for positive lookups except add a probe. If your workload mostly hits, the filter is a cost. This is exactly why RocksDB has the option optimize_filters_for_hits. The filter question is really a question about your read distribution.

No deletion. You cannot clear a bit, because other keys may share it. Counting Bloom filters replace each bit with a 4-bit counter, at 4 times the space. Or you move to the next structure.