Home › High-Level Design › Bloom Filters
B

Bloom Filters

Turn a slow, expensive "does this exist?" check into a tiny in-memory lookup that is wrong a known, tunable fraction of the time, and you can keep a database from drowning. That one sentence is the whole pitch, and the moment a candidate says "every request hits the database to check if the user exists," this is the structure I am waiting for them to reach for.

High-Level DesignIntermediate10 min read

If you remember five things

Everything below defends these five. The asymmetry in the first one is the entire reason this structure exists.

  1. One-sided error. A Bloom filter gives you "definitely not" (trust it completely) or "probably yes" (go verify). False positives happen, false negatives never. Flip that and you fail the question.
  2. Gate, not oracle. It sits in front of an exact source of truth and cheaply rules out the common case. Treat "probably yes" as a final answer and you have shipped a correctness bug.
  3. Two dials. Memory versus accuracy. About 10 bits per item buys roughly 1% false positives. More bits buy fewer, and you set both deliberately from your real n.
  4. Size for peak n. As the bit array fills, false positives climb toward useless. A filter built for 10M and loaded with 100M blows past 60%. There is no shrinking it either, because you cannot delete.
  5. Huge, bounded win. You trade a small, well-understood error for a large cost saving, and you only take the trade when a false positive is cheap to absorb.

1The core idea#

A Bloom filter answers exactly one question: is this item possibly in the set, or is it definitely not?

That phrasing is the whole structure. It is a probabilistic membership test, and it gives one of two honest answers:

  • "Definitely not." Trust it completely. The item was never added.
  • "Probably yes." It might be in the set, or it might be a false positive. You do not know yet.

The asymmetry is the point, not a weakness.

  • It never produces a false negative. It can produce a false positive.
  • Most of the time you are not trying to prove something is present.
  • You are trying to cheaply rule out the common case, so you only pay for expensive work when you have to.

The mental model I hand juniors: a bouncer with a bad memory who only ever errs toward letting people in.

  • "Never seen you, go home" is final. That is the "definitely not."
  • "Yeah, maybe, go check inside" means you still check inside. That is the "probably yes."
  • The win: the bouncer clears the overwhelming majority of obvious rejections for almost no cost.

And the cost really is almost nothing.

  • A filter for 100 million items at a 1% false positive rate fits in roughly 114 MB.
  • The raw items (100 million 64-character strings) are north of 6 GB just for the bytes.
  • A hash set holding them runs two or three times that again once you count pointers and load factor.
  • The price for that compression from gigabytes to 114 MB is the occasional false positive.
~114 MB
The whole set for 100M items at 1% errorThe same items as a hash set would cost well over 15 GB. This compression is the entire reason the filter fits in RAM on every node, and it is what you are buying with the false positive rate.

2How it works#

Under the hood it is almost embarrassingly simple: a big array of bits, all starting at 0, plus a handful of hash functions.

To add an item:

  • Run it through k independent hash functions. Each returns a position in the bit array.
  • Set those k bits to 1.
  • Adding "alice@example.com" with 3 hash functions might light up bits 17, 4902, and 88211.

To check an item:

  • Hash it the same way and look at those same k positions.
  • If any one of them is 0, the item was never added (adding it would have set every one), so you get a rock-solid "definitely not."
  • If all k bits are 1, you say "probably yes." Those bits could have been set by other items that collided, and that is exactly where false positives come from.
flowchart LR A[Incoming key] --> B[Hash k times] B --> C[Check k bit positions] C --> D{All bits set} D -->|No| E[Definitely absent] D -->|Yes| F[Probably present] E --> G[Skip the database] F --> H[(Database lookup)]
The check path. Notice the "No" branch is the whole value: a single 0 bit lets you skip the database entirely with certainty. The "Yes" branch is only a hint, so it always flows on to the real lookup.

A few things fall out of this design that matter in the room:

  • You cannot delete items. Clearing bits for one item might clear a bit another item still needs. If you need deletes, reach for a Counting Bloom filter (small counters instead of single bits) or a Cuckoo filter, which supports deletion directly.
  • You tune three knobs: n (expected items), m (number of bits), k (number of hash functions). Given n and a target false positive rate, the math hands you optimal m and k. The optimal hash count is roughly k = (m/n) * ln(2).
  • As the filter fills, false positives climb. Once most bits are 1, nearly everything reads "probably present." So you size for expected load up front.
  • You do not need k separate hash functions. The standard trick is double hashing: compute two good hashes and derive the rest as h1 + i*h2. Cheaper, and accuracy holds up fine in practice.
The one thing to lock in

The error is one-sided, and in exactly this direction: false positives yes, false negatives never. It falls straight out of the mechanics. A "definitely not" required at least one 0 bit, and adding the item would have set every one of its bits to 1. So a "no" is impossible to fake. Say this out loud and you have shown you understand the structure, not just memorized it.

A worked example with real numbers#

Let me size one end to end, because this is the calculation an interviewer loves to watch on the whiteboard. Say I am guarding a user-lookup path. I expect n = 10 million registered users, and I will accept a 1% false positive rate (p = 0.01).

The bit count: m = -(n * ln(p)) / (ln(2)^2).

  • Plugging in: -(10,000,000 * -4.605) / 0.4805, which is about 95.8 million bits.
  • That is roughly 9.6 bits per item, or about 11.4 MB. Tiny. It lives in RAM on every node with room to spare.

The hash count: k = (m/n) * ln(2) = 9.58 * 0.693, which rounds to 7 hash functions. So each add or check touches 7 bit positions.

Pause before the payoff: 95% of incoming lookups are for users who do not exist (bots scraping sequential IDs, stale links, typos). Roughly how much database traffic does this 11.4 MB filter remove?
Reveal the answer
About 94% of existence checks. The filter returns "definitely not" for essentially all 95% of non-existent lookups and skips the database entirely. The 5% of real users all pass through, plus the 1% false positive rate adds a thin trickle of unnecessary lookups. At 50,000 requests per second that is roughly 47,000 queries per second that never leave the application node. That is the difference between one modest read replica and a fleet of them.

Now the payoff, stated in numbers:

  • The filter returns "definitely not" for essentially all 95% of non-existent lookups and skips the database.
  • The 5% that are real all pass through, plus the 1% false positive rate adds a trickle of unnecessary lookups.
  • Net: existence checks drop by about 94%. At 50,000 requests per second that is roughly 47,000 queries per second that never leave the application node.

One number to internalize: size that same 11.4 MB filter and then load 100 million users into it, and the false positive rate climbs past 60%. The filter becomes worthless. Sizing for the real n is not optional.

3Trade-offs#

The central trade is memory versus accuracy, and both are dials you set deliberately.

A rough feel for the numbers:

  • About 10 bits per item buys roughly a 1% false positive rate.
  • Push to about 15 bits per item and you are near 0.1%.
  • Each extra factor of 10 in accuracy costs a few more bits per item, which is a great deal. Not free, though, and in a hot path with billions of items those bits add up.

Here is how I weigh a Bloom filter against the obvious alternatives.

Approach Memory False positives Deletes Good when
Bloom filter Tiny Yes, tunable No You need a cheap "definitely not" gate
Counting Bloom filter ~4x Bloom Yes Yes You need deletes and can spend the memory
Cuckoo filter Small Yes Yes You want deletes and better lookup locality
Hash set in memory Large None Yes The full set fits in RAM and you need exact answers
Database index lookup On disk None Yes You can afford the I/O per query

The thing people miss: a Bloom filter is rarely the whole answer.

  • It sits in front of an exact source of truth. It is a filter, not a replacement.
  • Treat "probably yes" as a final answer and you have a correctness bug waiting to happen.
  • What you give up: enumerating the set, cheap deletes, and trusting a positive.
  • What you gain: speed and a footprint small enough to keep on every node.
Red flag that fails you

Getting the asymmetry backwards. "It can have false negatives but no false positives" is an instant downgrade to anyone who knows the structure, because it is exactly inverted. The other failure is designing a system that acts on "probably yes" as if it were truth. Both say you have read about the filter but never placed one in a real system.

4Where it is used#

This shows up in production more than most people realize. The pattern is always the same: a cheap probabilistic gate protecting an expensive exact system.

  • Databases skipping disk reads. LSM-tree stores like Cassandra, RocksDB, and HBase keep a Bloom filter per SSTable. Before reading a file from disk, they ask the filter. "Definitely not" means skip that file entirely. On a store with 20 SSTables, a point read that would otherwise touch all 20 narrows to the one or two that might hold the key.
  • Caches avoiding useless lookups. The classic cache-penetration problem: attackers (or bad luck) request keys that do not exist, so every request misses the cache and hammers the database. A Bloom filter of known-valid keys in front means "definitely not" rejects without touching the backend.
  • Web and content systems. "Have I already shown this user this article?" or "is this URL already in the crawl frontier?" answered cheaply across billions of items. Early Chrome used one to check URLs against a safe-browsing list before the expensive exact check.
  • Deduplication at scale. Log pipelines and analytics systems drop likely-duplicate events before the expensive exact dedup stage.
flowchart LR A[Client] --> B[Service] B --> C{Bloom says present} C -->|No| D[Return not found] C -->|Yes| E[(Cache)] E -->|Miss| F[(Database)]
The gate pattern in one picture. The filter only ever short-circuits the "No" path (return not found, touch nothing). Every "Yes" still falls through to the cache and then the database, so the exact source of truth always has the final word.
From the interviewer's chair

I am usually not asking "what is a Bloom filter." I am watching whether you reach for one on your own when the prompt is screaming for it. Name the trigger out loud ("95% of these lookups are for keys that do not exist"), then put the filter in front of the right box. That unprompted move is worth more than a textbook definition.

5Common pitfalls and gotchas#

  • Undersizing the filter. Expect 1 million, build for 1 million, then load 10 million, and your false positive rate goes through the roof. We saw it: built for 10 million, loaded with 100 million, past 60%. Size for your real peak with headroom. When you genuinely do not know n in advance, use a scalable Bloom filter that chains new filters as it fills.
  • Treating a positive as truth. Say it with me: "probably yes" always means go verify. The only time you can act on a positive is when a false positive is harmless, like "might have seen this ad, so skip it," which costs one missed impression, not a bug.
  • Forgetting it cannot shrink. No deletes. If your set churns (items come and go), a plain filter slowly saturates and becomes useless. Rebuild periodically, or use a Counting or Cuckoo filter.
  • Weak or correlated hashes. Poorly distributed hashes make bits cluster and push false positives above the theoretical rate. Use a solid non-cryptographic hash like MurmurHash or xxHash with the double-hashing trick.
  • Serialization and sharing. In a distributed system the filter has to live somewhere consistent. If every node builds its own from a different data snapshot, they disagree. Decide up front: per-node, shared, or rebuilt from a source of truth, and version it.
  • The empty-filter edge case. A brand new filter says "definitely not" to everything, which is correct but gives zero protection until populated. During a cold start or rebuild, make sure the fallback path can take full load.

6How it shows up in interviews#

Interviewers bring up Bloom filters two ways. Sometimes directly ("explain a Bloom filter and its trade-offs"). More often they are waiting to see if you reach for one unprompted when the design is screaming for it.

Watch for these triggers in the prompt:

  • A high-volume existence check.
  • Cache penetration from non-existent keys.
  • Dedup across a huge stream.
  • "We cannot fit the whole set in memory but we check membership constantly."

The contrast I listen for.

A weak answer: "A Bloom filter is a probabilistic data structure. It can have false negatives but no false positives, so it tells you if something is in a set using hash functions."

  • That fails twice. It recites the definition and stops, and it gets the asymmetry backwards.
  • False negatives are the one thing a Bloom filter never produces. Flipping that is an instant red flag.

A strong answer quantifies and places it:

  • "Ninety-five percent of these lookups are for users who do not exist, so I would put an 11 MB filter sized for 10 million users at a 1% false positive rate in front of the database."
  • "That drops roughly 94% of existence checks before they touch storage, about 47,000 queries per second off the database at our load."
  • "A 'probably present' still goes to the real index, so correctness holds. I would size for peak with headroom, and because this key set only grows I am fine with a plain filter and no deletes."
  • "The one thing I would watch is the cold start: an empty filter protects nothing, so the fallback has to survive full load during a rebuild."

To really land it, name one concrete alternative and why you did or did not pick it. "Plain Bloom filter here because the key set only grows. If we had heavy churn I would switch to a Cuckoo filter for the deletes." That one sentence shows you treat the structure as an engineering choice, not a party trick.

A Bloom filter trades a small, bounded, well-understood error for a large, unbounded cost saving. You only take the trade when the false positive is cheap to absorb.

Follow-ups the interviewer will ask (and your answers)
"Why can a Bloom filter give false positives but never false negatives?"
Because a "definitely not" needs at least one of the item's k bits to be 0, and adding that item would have set every one of them to 1. So a stored item can never read as absent. A false positive happens the other way: other items set all k of some never-added item's bits, so it reads "probably present." The error is structurally one-sided.
"I need to delete items from the set. What do I do?"
A plain Bloom filter cannot delete, because clearing one item's bits might clear a bit another item still relies on. Switch to a Counting Bloom filter (small counters you increment on add and decrement on delete, roughly 4x the memory) or a Cuckoo filter, which supports deletion directly and has better lookup locality.
"How did you pick m and k for 10 million users at 1%?"
m = -(n * ln p) / (ln 2)^2, which gives about 95.8 million bits, roughly 9.6 bits per item or about 11.4 MB. Then k = (m/n) * ln 2, which rounds to 7 hash functions. In practice I derive those 7 positions from two hashes via double hashing rather than running 7 independent hash functions.
"The filter is cold after a rebuild. What breaks?"
Nothing breaks correctness. A new filter says 'definitely not' to everything, which is still accurate for an empty set, but it provides zero filtering, so every lookup falls through to the backend. The gotcha is capacity: the fallback path has to survive full load during the warm-up. I rebuild in the background and swap in the warm filter, or serve the old one until the new one is populated.
"We do not know n ahead of time. Now what?"
Use a scalable Bloom filter: start with one sized for a guess, and when it fills past its target rate, chain on a new, larger filter with a tighter per-filter error so the combined rate stays bounded. A check hits all filters in the chain. You pay a little extra memory and lookup cost to avoid the undersizing cliff where false positives run away.

What you can now explain in the room

  • Why the error is one-sided (false positives yes, false negatives never) and how that falls out of the bit mechanics.
  • Why a Bloom filter is a gate in front of a source of truth, never the source of truth itself.
  • How to size one end to end: m, k, bits per item, and the memory it costs.
  • Why undersizing is fatal and why the structure cannot delete or shrink.
  • Where it earns its keep in production: LSM-tree reads, cache penetration, dedup, URL frontiers.
  • When to pick a Counting or Cuckoo or scalable variant instead, and why.

Next up, try the System Design Question walkthrough on designing a web crawler, where a Bloom filter keeps the URL frontier from re-visiting billions of pages.

Get a prep plan built around your Bloom Filters interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Bloom Filters prep plan is on the way.