Skip to content

Everything DataInteractive lab

Bloom filter sizing

Pick bits per element and watch the false positives arrive

Insert keys, query keys you never inserted, and compare the measured false-positive rate against the one the formula predicted for your chosen bit count.

What this teaches

  • Bits per element
  • Hash count
  • False-positive rate

Size the filter

m = 50,000 bits · 6.1 KiB

The optimal hash count is (m/n) x ln2, which at 10 bits per element is 6.93, so 7. Below it the filter is under-probed and too many keys share a signature; above it every insert sets more bits and the array fills faster than the extra probe is worth. Both directions make the rate worse, which is why there is an optimum at all.

Measured against predicted

47 of 5,000 absent keys came back "maybe"
0.94%
Measured false positives
0.82%
Predicted by the formula
0.83%
Predicted from actual fill
50.4%
Bits set

At 10 bits per element with 7 hashes, 47 of 5,000 absent keys returned a false positive, a rate of 0.94% against a predicted 0.82%.

Before reading anything into the gap, check the band: with 5,000 queries at this rate the formula expects 41 false positives give or take 6, so anything between 28 and 54 is the same answer. 47 is inside it, so measured and predicted agree here. Raise the query count to shrink the band.

The middle number also assumes the k probes set k distinct bits. They do not - a key can probe the same bit twice - so slightly fewer bits get set than it expects. The third number feeds the actual count of set bits back into the same exponent, so it is the closer of the two whenever the fill drifts from what the formula assumed. That is the assumption becoming visible, not an error.

Read the other way: a 0.94% rate needs about 9.7 bits per element, from m/n = -log2(eps) / ln2. That identity has no n in it, which is the surprising part - the bits per element you need depends only on the rate you will tolerate, never on how many keys you plan to store.

The array itself

first 512 of 50,000 bits

This is a sample, not the whole array - at 10 bits per element and 5,000 keys there are 50,000 of them. Drag the bits-per-element slider down and watch it saturate: once nearly every bit is set, every query finds all k of its probes already true and the filter starts answering "maybe" to everything, which is the failure mode of an undersized filter.

Walk one query

k probes, and what each one found
The k probe positions for q-000001 and the bit found at each.
ProbePositionBit
i = 033,0361
i = 16,1310 - stop here
i = 229,2260 - stop here
i = 319,6171
i = 442,7120 - stop here
i = 515,8070 - stop here
i = 66,1981

One probe found a 0, so the filter says definitely absent and stops. It cannot be wrong about this: an inserted key would have set that bit.

The error is one-sided, and this panel is where that stops being a slogan. A zero is a proof - no insert could have left it clear. A run of ones is only an absence of evidence. That asymmetry is the entire reason a filter like this is safe to put in front of a disk: a "no" skips the read, and a "maybe" costs you nothing but the read you were going to do anyway.

Every size, re-run

each row is a fresh filter at its own optimal k
False-positive rate by bits per element, measured and predicted.
Bits/elementkmMeasuredFormulaFrom fill
4320,00013.54%14.69%14.59%
6430,0006.32%5.61%5.54%
8640,0002.10%2.16%2.09%
10750,0000.94%0.82%0.83%
12860,0000.34%0.31%0.31%
141070,0000.28%0.12%0.12%
161180,0000.02%0.05%0.04%
2014100,0000.02%<0.01%<0.01%

Every row here is a filter that was really built and really queried 5,000 times, not a formula evaluated at eight points - otherwise the table would only show the prediction agreeing with itself. Roughly 10 bits per element buys about 1%, and each further 4.8 bits divides the rate by ten - that second figure is the sizing identity again, since 1/ln2 x log2(10) is 4.8.

What is real here, and what is standing in

Real: the bit array is a real Uint8Array with real bit operations, the inserts really set bits, and the measured rate is a count of wrong answers divided by a count of queries. The double hashing is not a simplification either - deriving k probes from two hashes is the standard Kirsch-Mitzenmacher construction that real implementations use instead of computing k separate digests.

Standing in: the base hash is FNV-1a with MurmurHash3's finalizer, where production would reach for xxHash or MurmurHash3 proper. The keys are generated strings with a regular shape; a structured or adversarial key universe can do worse than anything shown here. And this is a plain Bloom filter - no counting variant, so nothing can be deleted, and no partitioned or scalable variant, so it cannot grow past the size you chose.

Everything on this page runs in your browser. Nothing you type is sent anywhere, there is no account, and it keeps working offline.