system-design · intermediate
Bloom Filters — Probabilistic Set Membership at Scale
The Central Question
Consider a high-throughput storage engine running on the DataLab platform (datalab.com):
- An LSM-Tree storage engine (such as RocksDB or Cassandra) stores 1,000,000,000 key-value pairs distributed across 500 immutable SSTable files on disk.
- A user issues a read query:
GET user_session_9012. user_session_9012does not exist in the database (a non-existent key).
Without a fast membership pre-filter:
- The database searches the in-memory MemTable (Key Not Found).
- The database initiates expensive disk reads, scanning index blocks across Level 0, Level 1, Level 2, and Level 3 SSTable files on disk.
- After executing 12 random disk page reads across 500 SSTables, the engine finally concludes that
user_session_9012does not exist.
Searching disk files for keys that do not exist causes massive Read Amplification and wastes disk bandwidth.
This lesson answers one central question: How does a Bloom Filter use a compact bit array and $k$ independent hash functions to provide a fast $O(1)$ in-memory test that guarantees a key is "definitely not in the database," bypassing expensive disk seeks without storing raw key strings in RAM?
The Probabilistic Guarantee: Zero False Negatives
A Bloom Filter is a probabilistic data structure. It does not store actual key strings or values; instead, it stores bit patterns generated by $k$ independent cryptographic hash functions.
flowchart TD
Query[Query Key: 'usr_9012'] --> Test{Evaluate Bloom Filter}
Test -->|Bit Pattern Returns 0| DefNot["DEFINITELY NOT IN SET!<br/>(Zero False Negatives Guaranteed!)"]
Test -->|Bit Pattern Returns 1| PossIn["POSSIBLY IN SET<br/>(Low False Positive Rate: p = 1%)"]
DefNot -->|Fast Bypass!| ReturnNull[Return Key Not Found Immediately<br/>Latency: 0.01ms | Zero Disk Seeks!]
PossIn -->|Perform Disk Seek| ReadDisk[Read MemTable & SSTable Files on Disk<br/>Latency: 2.0ms]
Figure 1: Decision logic for Bloom Filter set membership queries.
The Invariant Principles
- Zero False Negatives ($0\%$ Error): If the Bloom Filter returns
"Definitely Not in Set", the key is 100% guaranteed not to exist. The system can safely skip disk seeks. - Controlled False Positives ($p\%$ Error): If the Bloom Filter returns
"Possibly in Set", the key might exist, or hash collisions might have set those bits artificially. The database proceeds to check disk files.
Anatomy of a Bloom Filter: Bit Array & Hash Functions
A Bloom Filter consists of a Bit Array of size $m$ bits (all initialized to 0) and $k$ independent uniform hash functions ($h_1, h_2, \dots, h_k$).
flowchart TB
subgraph Step 1: Insertion of Key 'alice'
Key1["Key: 'alice'"] --> H1["h1('alice') = Bit Index 2"]
Key1 --> H2["h2('alice') = Bit Index 5"]
Key1 --> H3["h3('alice') = Bit Index 9"]
end
subgraph Step 2: Bit Array Modification (Size m = 12 bits)
Bits["Bit Array: [0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0]"]
end
H1 --> Bits
H2 --> Bits
H3 --> Bits
Figure 2: Inserting a key by setting bit positions 2, 5, and 9 to 1.
1. Item Insertion Algorithm
To insert a key (e.g. `"alice"`):- Compute $k$ hash values: $h_1(\text{"alice"}), h_2(\text{"alice"}), \dots, h_k(\text{"alice"})$.
- Map each hash value to a bit array index using modulo math: $\text{index}_i = h_i(\text{"alice"}) \pmod m$.
- Set the bit array entries at those $k$ calculated indexes to
1.
2. Item Membership Query Algorithm
To query whether a key exists (e.g. `"bob"`):- Compute the $k$ bit array indexes: $\text{index}_1, \text{index}_2, \dots, \text{index}_k$.
- Inspect the bit array at those indexes.
- If any of the $k$ bits contains a
0, return"Definitely Not in Set". - If all $k$ bits contain
1, return"Possibly in Set".
Mathematical Formulations: Optimal Sizing & Hash Count
Engineers configure Bloom Filters by defining two input requirements:
- $n$: The total number of items to store in the filter (e.g. $n = 10,000,000$ keys).
- $p$: The acceptable False Positive Probability (e.g. $p = 0.01$, or $1\%$).
flowchart LR
Inputs["Inputs: n = 10M keys, p = 1% error"] --> Math1["1. Calculate Optimal Bit Array Size (m)"]
Math1 --> Math2["2. Calculate Optimal Hash Functions (k)"]
Math2 --> Config["Result: m = 95.8 MB RAM, k = 7 Hash Functions"]
Figure 3: Deriving memory array size and hash function count from target parameters.
1. Optimal Bit Array Size Formula ($m$)
The required number of bits $m$ needed to achieve a target false positive rate $p$ for $n$ items is:$$m = -\frac{n \times \ln(p)}{(\ln 2)^2}$$
Because $(\ln 2)^2 \approx 0.48045$, the formula simplifies to:
$$m \approx -2.081 \times n \times \ln(p) \quad \text{bits}$$
- Example: To store $n = 10,000,000$ keys with a $p = 0.01$ ($1\%$) false positive rate:
2. Optimal Number of Hash Functions Formula ($k$)
The optimal number of hash functions $k$ that minimizes false positive errors for a given ratio of $m/n$ is:$$k = \frac{m}{n} \times \ln 2 \approx 0.693 \times \frac{m}{n}$$
- Example: With $m/n = 9.585$ bits per key:
Partitioned Bloom Filters vs Standard Filters
In high-throughput multi-threaded CPU environments, evaluating $k$ hash functions across a single large bit array can cause L1/L2 CPU cache misses because the calculated bit positions span distant memory addresses. To improve CPU cache locality, modern engines deploy **Partitioned Bloom Filters**. A Partitioned Bloom Filter divides the total bit array into $k$ equal segments of size $m/k$. Each hash function $h_i$ writes to its own isolated bit segment, allowing SIMD vector instructions to evaluate all $k$ bit checks in parallel with minimal CPU cache line thrashing.Why Standard Bloom Filters Cannot Handle Deletions
A standard Bloom Filter does not support item deletions.
flowchart TD
Del["Attempting to Delete Key 'alice' (Bits: 2, 5, 9)"] --> Question{Can we set Bit 5 back to 0?}
Question -->|NO!| Conflict["Bit 5 is shared by Key 'charlie' (Bits: 1, 5, 11)!<br/>Setting Bit 5 to 0 deletes 'charlie' silently!"]
Figure 4: Shared bit positions preventing key deletion in standard Bloom Filters.
If an application attempts to delete a key by clearing its $k$ bits to 0, it will inadvertently clear bits shared by other active keys, introducing False Negatives and corrupting the fundamental invariant.
Solution: Counting Bloom Filters
To support deletions, systems deploy a **Counting Bloom Filter**. Instead of a single bit (`0` or `1`), each array slot contains a 4-bit integer counter.- Insert: Increment counters at the $k$ index positions ($+1$).
- Delete: Decrement counters at the $k$ index positions ($-1$).
- Trade-off: Consumes 4x to 8x more RAM than a standard bit array.
Real-World Production Use Cases
flowchart TD
UseCases[Production Applications of Bloom Filters] --> UC1[1. LSM Storage Engines]
UseCases --> UC2[2. Web Search & Web Crawlers]
UseCases --> UC3[3. CDN Edge Cache Bypassing]
UseCases --> UC4[4. Malicious URL Security Filters]
UC1 --> UC1a[RocksDB / Cassandra skip non-existent SSTable files on disk]
UC2 --> UC2a[Google Web Crawler skips millions of previously crawled URLs]
UC3 --> UC3a[Cloudflare checks if URL exists in cache before triggering disk I/O]
UC4 --> UC4a[Chrome checks target domain against 1M malicious site hashes in RAM]
Figure 5: Industry use cases for high-scale Bloom Filter implementations.
Complete Worked Example: Go Thread-Safe Bloom Filter Implementation
Let's inspect the complete Go implementation of a memory-efficient Bloom Filter using MurmurHash3 for the DataLab engine (datalab.com).
package main
import (
"fmt"
"math"
"sync"
"github.com/spaolacci/murmur3"
)
type BloomFilter struct {
mu sync.RWMutex
bitArray []bool
m uint64 // Bit array size
k uint64 // Number of hash functions
itemCount uint64
}
func NewBloomFilter(expectedItems uint64, falsePositiveRate float64) BloomFilter {
// m = - (n ln(p)) / (ln(2)^2)
m := uint64(math.Ceil(-1 float64(expectedItems) math.Log(falsePositiveRate) / math.Pow(math.Log(2), 2)))
// k = (m / n) ln(2)
k := uint64(math.Round(float64(m) / float64(expectedItems) math.Log(2)))
return &BloomFilter{
bitArray: make([]bool, m),
m: m,
k: k,
}
}
// Double Hashing technique: hash_i(x) = (hash1(x) + i hash2(x)) % m
func (bf BloomFilter) getHashes(key string) []uint64 {
h1, h2 := murmur3.Sum128([]byte(key))
indexes := make([]uint64, bf.k)
for i := uint64(0); i < bf.k; i++ {
combined := h1 + (i * h2)
indexes[i] = combined % bf.m
}
return indexes
}
func (bf *BloomFilter) Add(key string) {
bf.mu.Lock()
defer bf.mu.Unlock()
indexes := bf.getHashes(key)
for _, idx := range indexes {
bf.bitArray[idx] = true
}
bf.itemCount++
}
func (bf *BloomFilter) Contains(key string) bool {
bf.mu.RLock()
defer bf.mu.RUnlock()
indexes := bf.getHashes(key)
for _, idx := range indexes {
if !bf.bitArray[idx] {
return false // DEFINITELY NOT IN SET! (Zero False Negatives)
}
}
return true // POSSIBLY IN SET (Subject to p% false positive rate)
}
Failure Modes and Engineering Mitigations
| Failure Scenario | Root Cause | System Symptom | Detection Metric | Mitigation Strategy |
|---|---|---|---|---|
| 1. Bloom Filter Over-Saturation | Inserting 100,000,000 items into a Bloom Filter sized for 1,000,000 items. | Bit array becomes 100% 1s; false positive rate hits 100%; all requests trigger disk reads. | Elevated bloom_filter_false_positive_rate metrics ($> 50\%$). | Size filters conservatively or implement Dynamic Scalable Bloom Filters that add bit arrays automatically. |
| 2. Silent False Negative Bug | Attempting to clear bits in a standard Bloom Filter during item deletion. | Valid existing items return "Definitely Not in Set"; database returns missing data errors. | Data inconsistency reports in background consistency audits. | Use Counting Bloom Filters or rebuild the filter from scratch during SSTable compaction. |
| 3. Non-Uniform Hash Function Bottleneck | Using weak hash functions (e.g. CRC32 or modulo additions) causing hash clustering. | False positive rate is significantly higher than theoretical predictions. | High hash collision count metrics during insertion. | Use MurmurHash3 or CityHash combined with Double Hashing ($h_i(x) = h_1 + i \times h_2$). |
| 4. High Memory Footprint on Micro-Nodes | Allocating massive static 1 GB Bloom Filters on low-RAM container instances. | Worker node crashes due to Out-Of-Memory (OOM) kernel kills. | Spikes in container OOM kill restarts. | Size Bloom Filters using exact mathematical formulas based on expected item counts ($n$). |
What You Should Remember
- Zero False Negatives: A Bloom Filter guarantees that if a key returns
"Definitely Not in Set", the key is 100% absent from storage ($0\%$ error). - Controllable False Positives: Returns
"Possibly in Set"with an error rate $p$ dictated by bit array size $m$ and hash count $k$. - Massive Memory Savings: Bloom Filters store bit patterns, consuming 95% less RAM than raw hash tables.
- Standard Bloom Filters cannot delete: Clearing bits during deletion corrupts shared key bits. Use Counting Bloom Filters or rebuild filters during compaction.
- Essential for LSM Storage Engines: RocksDB and Cassandra use Bloom Filters to skip non-existent SSTable disk files, avoiding costly disk reads.
Glossary of Terms
| Term | Definition |
|---|---|
| Bloom Filter | A space-efficient probabilistic data structure used to test set membership. |
| False Positive | A query result indicating an item is present in the set when it is actually absent. |
| False Negative | A query result indicating an item is absent when it is actually present (Forbidden in Bloom Filters!). |
| Bit Array ($m$) | The underlying memory array of size $m$ storing binary 0 and 1 bits. |
| Hash Count ($k$) | The number of independent hash functions used to map a key to array bit positions. |
| Counting Bloom Filter | A Bloom Filter variant that replaces bits with numeric counters to support item deletions. |
Practice Scenario and Self-Assessment
Architecture Scenario
You are designing a web security crawler (`securitylab.com`):- The crawler tracks 500,000,000 malicious URL strings.
- The crawler must check incoming URLs against the database in RAM with sub-1ms response times.
- Target False Positive Probability: $p = 0.001$ ($0.1\%$).
- Calculate the required Bit Array size ($m$) in Megabytes to store all 500,000,000 malicious URLs.
- Explain why a standard Bloom Filter is superior to an in-memory Redis Hash Table for this security crawler.
Interactive Self-Assessment
The requested key is 100% guaranteed not to exist in storage (Zero False Negatives).
The requested key has a 1% chance of existing in storage.
The storage engine must re-format its hard drive file system.
The network socket terminates its HTTPS TLS encryption handshake.
Multiple keys share identical bit positions; clearing a bit for Key A deletes shared bits for Key B.
Clearing bits causes CPU hardware clock skew on the database server.
Standard Bloom Filters use 64-bit float values that cannot be decremented.
Clearing bits causes memory locks that freeze OS kernel threads.
What to Learn Next
- Database Storage Architectures — B-Trees vs LSM-Trees: See how RocksDB and Cassandra integrate Bloom Filters with SSTables.
- Types of Databases — Matching Tools to Access Patterns: Revisit database paradigms and data structures.
- Content Delivery Networks (CDN) — Edge Acceleration: Learn how CDNs use Bloom Filters to check edge cache membership.
Track: Data, Storage and Messaging
Previous: Banking Transaction Platform Design
Next: Connection Pooling — Reusing Expensive Database Sessions
Series: Caching
- Caching 101 — Memory Offloading and Latency Reduction
- Caching Strategies — Aside, Through, Behind, and Refresh-Ahead
- Cache Eviction Policies — LRU, LFU, TTL, and Friends
- Distributed Caching — Sharding and High-Availability Clusters
- Cache Stampede — When Expiry Melts the Database
- Stale Cache After Write — When Your Own Update Disappears
- Content Delivery Networks (CDN) — Edge Acceleration and Caching
- Bloom Filters — Probabilistic Set Membership at Scale (this guide)
By Shubham Jain