Bloom filter
A tiny bit array that answers "have I seen this before?" with "definitely not" or "maybe", using a few hash functions and far less memory than a set.
O(k * L) per add or checkSpace O(size) bits3 practice problemsStep-through pictureThe idea
A set that stores every word you have seen can grow huge. A Bloom filter stores only a fixed array of bits (0s and 1s), no matter how many words you add. The price: it sometimes says "maybe" for a word you never added. It never says "no" for a word you did add.
To add a word, run it through a few different hash functions. A hash function turns text into a number, always the same number for the same text; here each number is a position in the bit array. Set the bit at every one of those positions to 1.
To check a word, compute the same positions. If any of those bits is 0, the word was definitely never added, because adding it would have set that bit. If all are 1, the word was probably added, but other words may have set those bits by chance. That chance is called the false positive rate.
An everyday example
A guest book with ink stamps
At a party, every guest presses three rubber stamps onto one shared sheet of paper; the stamps a guest uses depend on their name. Later, to check whether someone came, you look for their three stamps. If any is missing, they certainly didn't come. If all three are there, they probably came, but three other guests may have left those same stamps between them.
| In the example | In the code |
|---|---|
| The shared sheet | this.bits |
| Which stamps a name uses | this.hash(text, seed) for each seed |
| Pressing the stamps | this.bits[...] = 1 |
| A stamp is missing: certainly not here | some bit is 0, so mightContain is false |
| All stamps there: probably here | every bit is 1, so mightContain is true |
From the slow way to bloom filter
- The exact way to answer "have I seen this before?" is a
Setof everything seen. Its memory grows with every item: 10 million web addresses of about 50 characters each is well over 500 MB. That is too big to ship to a browser or to keep in fast memory, even though each answer is just yes or no. - Suppose we may be wrong in one direction only: "no" must always be true, but "yes" may sometimes be a mistake that a slower exact check can catch later. Then we don't need to store the items at all. Hash each item to a position in a fixed array of bits and set that bit to
1. A check that finds a0bit knows for sure the item was never added. - With one hash function, two different items land on the same bit often, so "yes" is wrong too often. Use
khash functions instead: adding an item setskbits, and a check says "maybe" only when allkof its bits are1. An item that was never added would have to hitkset bits by chance, which is much less likely than hitting one. - The rest is tuning. With
mbits,nitems, andkhash functions, the chance of a wrong "maybe" is about(1 - e^(-k × n / m))^k. About 10 bits per item with 7 hash functions gives about 0.8%. A fixed bit array,khashes per word, and an "all bits set" check: that is a Bloom filter.
How it works
- Create
bits, an array ofsizezeros, and choose a few hash functions. Here each one is the same formula with a differentseed. hash(text, seed): start withh = 0; for each character,h = (h * seed + charCode) % size. The result is a bit position.add(text): for every seed, setbits[hash(text, seed)] = 1.mightContain(text): for every seed, look atbits[hash(text, seed)].- If any of them is
0, returnfalse(definitely not added). If all are1, returntrue(maybe added).
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
class BloomFilter {constructor(size, seeds) {this.bits = new Array(size).fill(0);this.seeds = seeds;}hash(text, seed) {let h = 0;for (const char of text) {h = (h * seed + char.charCodeAt(0)) % this.bits.length;}return h;}add(text) {for (const seed of this.seeds) this.bits[this.hash(text, seed)] = 1;}mightContain(text) {return this.seeds.every((seed) => this.bits[this.hash(text, seed)] === 1);}}
How fast it is
| Time | O(k * L) per add or check |
|---|---|
| Extra memory | O(size) bits |
Each operation computes k hashes over a word of L characters, regardless of how many words were added. The bit array never grows.
- Best case
O(L)A check whose first position holds
0, like"snow":everystops after one hash of theLcharacters. - Average case
O(k × L)Every
add, which always computes allkhashes, and checks of words that were added or nearly match. - Worst case
O(k × L)A check that answers
true, such as"lime", computes allkhashes. The cost never depends on how many words were added.
Common mistakes
- Treating
trueas "definitely present". It only means "maybe"; confirm with an exact lookup if it matters. - Trying to remove a word by clearing its bits, which can make other added words report "no". Use a counting filter for removal.
- Using hash functions that are not independent (for example the same seed twice), which gives the false positive rate of fewer functions.
- Making the bit array too small for the number of items, so almost every bit becomes
1and every check says "maybe".
When to use it
Look for these clues in a problem:
- You need a fast "have we seen this?" check over a huge set, and memory is tight.
- A wrong "maybe" is acceptable because a slower exact check can follow, but a wrong "no" is not.
- Items are only added, never removed (or use a counting variant).
- The task gives you exact hash functions and asks which bits are set or what a check returns.
Check yourself
Answer each one in your head first, then open it.
In the trace, why does mightContain("lime") return true?
Its positions are 1 and 10. Bit 1 was set by sand and bit 10 by red, so both are 1 even though lime itself was never added.
Why can a Bloom filter never answer "no" for a word that was added?
Adding a word sets all its positions to 1, and no operation ever sets a bit back to 0. So every later check finds all 1s.
How many hashes does mightContain("snow") compute, and why?
Just 1. Its first position, bit 2, is 0, and every stops at the first false.
Why can't you remove red by clearing bits 10 and 12?
Other words may use those bits too. Clearing bit 10 here would make mightContain("lime") say false, and the same could happen to a word that really was added, breaking the "no is always right" promise.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 3 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The filter keeps one property: a bit, once 1, stays 1. No line of the code ever sets a bit to 0. So after add(text), every one of text's k positions is 1, and they stay 1 no matter what is added later. The hash functions always give the same positions for the same text, so mightContain(text) finds all 1s and returns true. That's why an added item never gets a "no".
So a "no" is always right. If mightContain finds a 0 at one of text's positions, then add(text) can't have run, because it would have set that bit and nothing could have cleared it. The answer "definitely not added" follows straight from the property.
A "yes" is only likely. Other words share the same bit array, and together they may have set every position of a word that was never added, as with lime in the trace. The more bits are 1, the more often that happens, which is why the size must grow with the number of items. A good rule: m ≈ -n × ln(p) / (ln 2)² bits for a wrong-"maybe" rate p, which is about 9.6 bits per item for 1%, with k ≈ (m / n) × ln 2 hash functions.
Going deeper
3 more ideas
Why "no" is always right
Bits only ever change from 0 to 1. When a word is added, all of its positions become 1 and stay 1. So if a check finds a 0 at one of a word's positions, that word can't have been added. A "yes" can be wrong because different words can share positions.
Choosing the size
More bits and a sensible number of hash functions lower the false positive rate. A common rule of thumb: about 10 bits per item with 7 hash functions gives roughly a 1% false positive rate, far less memory than storing the items themselves. Too many hash functions fill the array quickly; too few make collisions likely.
Cost and limits
Adding and checking compute k hashes over a word of length L, so each is O(k * L), independent of how many words were added. Memory is fixed at size bits. You can't list the words back, and you can't remove one by clearing bits, because other words may share them; a counting Bloom filter keeps small counters instead of bits to allow removal.
Work it by hand
Every step in a table, with why
A 13-bit filter with two hash functions (seeds 5 and 9). Add three words, then check four. The bits column shows index 0 on the left and index 12 on the right.
size = 13, seeds = [5, 9]
| Step | Call | Positions for seeds 5, 9 | bits after | Result | Why |
|---|---|---|---|---|---|
| 1 | add("red") | 10, 12 | 0000000000101 | - | For seed 5, h goes 114 % 13 = 10, then (10 × 5 + 101) % 13 = 8, then (8 × 5 + 100) % 13 = 10. Seed 9 gives 12. Both bits become 1. |
| 2 | add("sand") | 4, 1 | 0100100000101 | - | Two more bits are set. The filter now has 4 of 13 bits on. |
| 3 | add("jade") | 0, 7 | 1100100100101 | - | After three words, 6 of the 13 bits are 1. |
| 4 | mightContain("red") | 10, 12 | unchanged | true | Both bits are 1, as they must be for a word that was added. |
| 5 | mightContain("blue") | 10, 3 | unchanged | false | Bit 10 is 1, set by red, but bit 3 is 0. blue was definitely never added. |
| 6 | mightContain("lime") | 1, 10 | unchanged | true | A false positive: bit 1 came from sand and bit 10 from red. lime was never added, but the filter can't tell. |
| 7 | mightContain("snow") | 2, - | unchanged | false | Bit 2 is 0, so every stops right there and the second hash is never computed. |
The filter answered true for red, which was added, false for blue and snow, which is always right, and true for lime, which is a false positive. With 6 of 13 bits set, about 21% of all three-letter strings would get a wrong "maybe" from this tiny filter, which is why real filters use far more bits per item.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Bloom filter | O(1) | O(n) | Per add or check: k hashes of the word, however many items were added. About 10 bits per item for a 1% wrong-"maybe" rate, when memory matters and a wrong "maybe" is acceptable. |
| Hash set | O(1) | O(n) | You need exact answers, or need to list or remove items. It stores every item, so it uses far more memory per item. |
| Sorted array with binary search | O(log n) | O(n) | The set is fixed, you also need "what is the nearest item?", and items are small enough to store. |
| Scan a list | O(n) | O(n) | Only for a few items, where simplicity wins. |
Extra memory: O(size), fixed when the filter is made. This code keeps each bit as a number in a normal array for clarity; a real filter packs 8 bits into each byte of a Uint8Array.
How the steps grow
- Bloom filter, Hash set
O(1) - Sorted array with binary search
O(log n) - Scan a list
O(n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Bloom filter | 1 | 1 | 1 |
| Hash set | 1 | 1 | 1 |
| Sorted array with binary search | 3 | 10 | 20 |
| Scan a list | 10 | 1,000 | 1 million |
Inputs that need care
5 tricky inputs
- The empty string
"" - The loop never runs, so
hashreturns0for every seed. Afterjadeset bit 0,mightContain("")returnstrueeven though""was never added. Reject empty input if that matters. - The same word added twice
- Its bits are already
1, so nothing changes. A Bloom filter can't count how many times a word was added. - The same seed twice, such as
[5, 5] - Both hashes give the same position, so the filter acts like it has one hash function and false positives become much more common. Use different seeds.
- A seed that is a multiple of
size, such as13with 13 bits (h × 13 + code) % 13throws awayh, so only the last character matters and words ending in the same letter always collide. Pick seeds that share no factor withsize.- Far more words than the filter was sized for
- Almost every bit becomes
1, and almost every check says "maybe". Size the filter for the most items you expect, or start a new, bigger filter when it fills.
Variations and follow-ups
Twists interviewers add
Counting Bloom filter
Store a small counter instead of a bit at each position. add increases the k counters, a remove decreases them, and a check looks for counters above 0. Removal now works, at the cost of a few bits per position instead of one.
Two hashes make many
Instead of k separate hash functions, compute two, h1 and h2, and use (h1 + i × h2) % size as the i-th position. The false positive rate stays almost the same and each word is only hashed twice.
Growing filters
When a filter reaches its planned number of items, freeze it and start a new one with more bits and a stricter target rate. A check asks every filter, and says "maybe" if any of them does.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "huge set", "limited memory", "probably seen", or "skip an expensive lookup when we know the answer is no". If false positives are not allowed, it's the wrong tool.
- Explain the one-way promise: bits only turn on, so "no" is certain and "yes" is "maybe". Then explain where false positives come from: other items' bits.
- Work a tiny example by hand, like the trace: show one true "yes", one certain "no", and one false positive. It proves you understand all three outcomes.
- Expect "how do you delete?". Answer: you can't clear bits, because other items may share them; use a counting Bloom filter with small counters instead.
- Expect "how big should it be?". Answer with the rule of thumb: about 10 bits per item and 7 hash functions for a 1% rate, or
m = -n × ln(p) / (ln 2)²in general.
Where frontend code uses it
The same idea in real UI code
- Browsers have used Bloom-filter-like structures to check URLs against large blocklists locally, only asking a server when the filter says "maybe".
- A feed or recommendation widget can keep a small Bloom filter of item ids the user has already seen, to skip repeats without storing every id.
- Some CDNs and databases use Bloom filters to skip disk lookups for keys that are definitely not stored.