How to Build a Bloom Filter to Reduce Cache Misses in Python
Implement a probabilistic Bloom filter in Python that lets a cache quickly determine which keys are definitely not present, reducing expensive source lookups on cache misses.
Python code
61 linesimport hashlib
import random
class BloomFilter:
def __init__(self, size=100, num_hashes=3):
self.size = size
self.num_hashes = num_hashes
self.bit_array = [0] * size
def _hashes(self, item):
result = []
for i in range(self.num_hashes):
hash_value = int(hashlib.md5(f"{i}:{item}".encode()).hexdigest(), 16)
result.append(hash_value % self.size)
return result
def add(self, item):
for index in self._hashes(item):
self.bit_array[index] = 1
def contains(self, item):
return all(self.bit_array[index] == 1 for index in self._hashes(item))
class Cache:
def __init__(self, capacity=5):
self.capacity = capacity
self.store = {}
self.bloom = BloomFilter()
def get(self, key):
if key in self.store:
print(f"CACHE HIT: {key}")
return self.store[key]
if self.bloom.contains(key):
print(f"CACHE MISS + BLOOM POSITIVE: {key} (likely in cache but evicted)")
else:
print(f"CACHE MISS + BLOOM NEGATIVE: {key} (definitely not cached)")
value = self._load_from_source(key)
self._cache(key, value)
return value
def _load_from_source(self, key):
return f"data_{key}"
def _cache(self, key, value):
self.store[key] = value
self.bloom.add(key)
if len(self.store) > self.capacity:
evicted_key = next(iter(self.store))
del self.store[evicted_key]
print(f"EVICTED: {evicted_key}")
if __name__ == "__main__":
cache = Cache(capacity=3)
for key in ["a", "b", "c", "d", "a", "e"]:
cache.get(key)
Output
CACHE MISS + BLOOM NEGATIVE: a (definitely not cached)
CACHE MISS + BLOOM NEGATIVE: b (definitely not cached)
CACHE MISS + BLOOM NEGATIVE: c (definitely not cached)
CACHE MISS + BLOOM NEGATIVE: d (definitely not cached)
EVICTED: a
CACHE HIT: a
EVICTED: b
CACHE MISS + BLOOM POSITIVE: e (likely in cache but evicted)
How it works
The Bloom filter uses hashlib.md5 to generate multiple hash positions for each key, setting bits in a fixed-size array. When checking membership, it returns True only if all bits are set — false positives are possible, but false negatives are impossible. The cache stores a small LRU-like dict and adds keys to the Bloom filter even after eviction, so it can distinguish 'definitely not seen' from 'possibly evicted'. This avoids hitting the source for keys the Bloom filter knows are new, cutting latency on repeated misses. The add operation is O(k) where k is the number of hashes, keeping both insert and lookup cheap for high-throughput caches.
Common mistakes
- Recreating a Bloom filter per cache entry instead of one shared instance
- Using a single hash function, which drastically increases false-positive rate
- Forgetting that Bloom filters can't delete keys — evicted entries stay marked
- Choosing a too-small bit array, causing collision-heavy false positives
Variations
- Use `pybloom_live` or `bloom_filter2` pip packages for production-optimized implementations
- Serialize the bit array to Redis with SETBIT/GETBIT for a distributed cache filter
Real-world use cases
- Reduce database queries in front-end caches by skipping lookups for keys never requested before.
- Prevent thundering herd problems when many clients request the same missing cache key simultaneously.
- Filter out known-invalid user IDs or tokens before hitting an auth service or rate limiter.
Sponsored
More from Caching & Redis
- Cache Asides in Python with a Read-Through Loader easy
- Cache Data in Redis with Python easy
- Cache Penetration Null Object Mock in Python medium
- Cache Stampede Prevention with SingleFlight in Python medium
- Cache Warming with Python: Preload Hot Keys easy
- Coalescing duplicate in-flight requests: one shared result for concurrent callers hard
Keep learning
Related tutorials and quizzes for this topic.