Chapter 29: Probabilistic Data Structures
1. Why Probabilistic
Probabilistic data structures trade perfect accuracy for massive improvements in space and time complexity. They provide approximate answers with bounded, quantifiable error rates.
| Problem | Deterministic Approach | Space | Probabilistic Approach | Space |
|---|---|---|---|---|
| Set membership (1B items) | HashSet | ~8 GB | Bloom Filter (1% FPR) | ~1.2 GB |
| Frequency counting (1M items) | HashMap | ~40 MB | Count-Min Sketch | ~200 KB |
| Cardinality (1B uniques) | HashSet | ~8 GB | HyperLogLog | 12 KB |
| 95th percentile (1B values) | Sorted array | ~4 GB | t-digest | ~10 KB |
Key Properties
| Property | Description | Example |
|---|---|---|
| Sublinear Space | Uses much less space than number of elements | HyperLogLog: O(log log n) bits |
| Bounded Error | Error rate is configurable and guaranteed | Bloom: 1% false positive rate |
| One-Way Errors | Errors only in one direction | Count-Min: over-estimates, never under |
| Constant Time Ops | Insert/query independent of data size | O(k) for k hash functions |
| Mergeable | Combine sketches from distributed sources | HLL sketches can be unioned |
2. Bloom Filter
A space-efficient probabilistic data structure for set membership testing. Uses a bit array and multiple hash functions.
Structure
Operations
class BloomFilter:
def __init__(self, m, k):
self.m = m # bit array size
self.k = k # number of hash functions
self.bits = [0] * m
def add(self, item):
for i in range(self.k):
index = hash_i(item, i) % self.m
self.bits[index] = 1
def contains(self, item):
for i in range(self.k):
index = hash_i(item, i) % self.m
if self.bits[index] == 0:
return False # Definitely not in set
return True # Probably in set (might be false positive)
False Positive Rate
P(false positive) ≈ (1 - e^(-kn/m))^k
Optimal k (minimize FPR):
k = (m/n) × ln(2) ≈ 0.693 × m/n
Required bits for target FPR:
m = -n × ln(p) / (ln(2))^2
where p = desired false positive rate
Parameter Selection
| n (items) | Target FPR | m (bits) | k (hashes) | Bits per Item |
|---|---|---|---|---|
| 1,000,000 | 1% | 9,585,058 | 7 | 9.6 |
| 1,000,000 | 0.1% | 14,377,588 | 10 | 14.4 |
| 1,000,000 | 0.01% | 19,170,117 | 13 | 19.2 |
| 10,000,000 | 1% | 95,850,584 | 7 | 9.6 |
Pros
- Constant O(k) insert and query time
- Space-efficient: ~10 bits per item for 1% FPR
- No false negatives
- Simple to implement
Cons
- Cannot delete items (basic version)
- False positives increase as filter fills
- Cannot list items in set
- Fixed size (must know n upfront)
3. Bloom Filter Applications
| Application | Problem Solved | Impact | Real-World Example |
|---|---|---|---|
| Cache Prefetching | Avoid expensive DB lookups for missing keys | Eliminates 99% of cache misses | Chrome uses for malicious URL detection |
| Duplicate Detection | Quick check if URL/email already seen | 10x throughput increase | Web crawlers avoid re-crawling pages |
| Database Query Optimization | Skip partition scans that can't have data | 50-90% reduction in disk I/O | Cassandra SSTables, Parquet row groups |
| Network Routers | Fast IP blacklist lookup | Wire-speed filtering | DDoS mitigation systems |
| Spell Checking | Quick dictionary membership test | Reduces full dictionary lookups | First-pass filter before edit distance |
| Weak Password Detection | Check against billions of leaked passwords | 200 MB vs 20 GB dictionary | Troy Hunt's Pwned Passwords API |
Cassandra SSTable Bloom Filters
// Query: SELECT * FROM users WHERE user_id = 12345
// Without Bloom filter: Check all 10 SSTables on disk
// With Bloom filter:
// - 8 SSTables return "definitely not present" (skip disk I/O)
// - 2 SSTables return "maybe present" (read these)
// Result: 80% reduction in disk reads
// Configuration
CREATE TABLE users (
user_id bigint PRIMARY KEY,
name text
) WITH bloom_filter_fp_chance = 0.01; // 1% FPR
Web Crawler Architecture
contains URL?} B -->|Yes - skip| C[Next URL] B -->|No - probably new| D[Fetch Page] D --> E[Add to Bloom] D --> F[Extract Links] F --> A style B fill:#ffe6cc
4. Counting Bloom Filter
Extension of Bloom filter that supports deletion by replacing each bit with a small counter (typically 4 bits).
Structure Comparison
| Feature | Standard Bloom | Counting Bloom |
|---|---|---|
| Storage per position | 1 bit | 4 bits (counter 0-15) |
| Insert operation | Set bit to 1 | Increment counter |
| Delete operation | Not supported | Decrement counter |
| Query operation | All bits == 1? | All counters > 0? |
| Space overhead | 1x | 4x |
| Counter overflow risk | N/A | Yes (max 15 with 4-bit) |
Implementation
class CountingBloomFilter:
def __init__(self, m, k, counter_bits=4):
self.m = m
self.k = k
self.max_count = (1 << counter_bits) - 1 # 15 for 4-bit
self.counters = [0] * m
def add(self, item):
for i in range(self.k):
index = hash_i(item, i) % self.m
if self.counters[index] < self.max_count:
self.counters[index] += 1
def remove(self, item):
# First verify item is present
if not self.contains(item):
return False
for i in range(self.k):
index = hash_i(item, i) % self.m
if self.counters[index] > 0:
self.counters[index] -= 1
return True
def contains(self, item):
for i in range(self.k):
index = hash_i(item, i) % self.m
if self.counters[index] == 0:
return False
return True
Counter Overflow Problem
- Load factor (n/m)
- Number of hash functions (k)
- Counter size (bits)
For 4-bit counters (max 15):
- At 50% load: overflow risk ~0.001%
- At 80% load: overflow risk ~1%
- At 95% load: overflow risk ~15%
Use Cases
- LRU cache with dynamic eviction
- Network flow tracking with timeouts
- Session management
- Sliding window duplicate detection
Tradeoffs
- 4x space overhead vs standard Bloom
- Still get false positives
- Counter overflow causes false negatives
- More complex than standard Bloom
5. Count-Min Sketch
Probabilistic data structure for frequency estimation. Uses a 2D array with multiple hash functions to estimate item counts with over-estimation guarantee.
Structure
Algorithm
class CountMinSketch:
def __init__(self, width, depth):
self.w = width # w columns
self.d = depth # d rows (hash functions)
self.table = [[0] * width for _ in range(depth)]
def add(self, item, count=1):
for i in range(self.d):
index = hash_i(item, i) % self.w
self.table[i][index] += count
def estimate(self, item):
estimates = []
for i in range(self.d):
index = hash_i(item, i) % self.w
estimates.append(self.table[i][index])
return min(estimates) # Return minimum to reduce over-estimation
Error Guarantees
count(item) ≤ estimate(item) ≤ count(item) + ε × N
where:
- N = total count of all items
- ε = error bound
- δ = failure probability
Parameters:
w = ⌈e / ε⌉ (e ≈ 2.718)
d = ⌈ln(1/δ)⌉
Example: ε=0.01 (1% error), δ=0.01 (99% confidence)
w = 272, d = 5
Space: 272 × 5 = 1,360 counters
Parameter Selection
| Error (ε) | Confidence (1-δ) | Width (w) | Depth (d) | Space (4-byte counters) |
|---|---|---|---|---|
| 1% of N | 99% | 272 | 5 | 5.3 KB |
| 0.1% of N | 99% | 2,718 | 5 | 53 KB |
| 1% of N | 99.9% | 272 | 7 | 7.4 KB |
| 0.01% of N | 99.99% | 27,183 | 9 | 955 KB |
6. Count-Min Sketch Applications
| Application | Use Case | Why CMS | Example |
|---|---|---|---|
| Heavy Hitters | Find top-K most frequent items | O(1) updates, sublinear space | Most viewed videos on YouTube |
| Network Traffic Analysis | Track packet counts per IP | Wire-speed processing | DDoS detection (find IPs with >10K packets/sec) |
| Rate Limiting | Enforce per-user API quotas | Over-estimation is safe (blocks slightly early) | Cloudflare rate limiting |
| Database Query Stats | Track query execution counts | Lightweight vs full query log | PostgreSQL slow query tracking |
| Ad Click Fraud | Detect users clicking same ad repeatedly | Scales to billions of events | Flag if user_id + ad_id > 100 clicks |
Heavy Hitters Algorithm
class HeavyHitters:
def __init__(self, k, epsilon, delta):
self.k = k
self.cms = CountMinSketch(width=int(2.718/epsilon), depth=int(math.log(1/delta)))
self.heap = [] # Min-heap of (count, item)
def add(self, item):
self.cms.add(item)
count = self.cms.estimate(item)
# If item in heap, update it
# If heap not full or count > min, add to heap
if len(self.heap) < self.k:
heapq.heappush(self.heap, (count, item))
elif count > self.heap[0][0]:
heapq.heapreplace(self.heap, (count, item))
def get_top_k(self):
return sorted(self.heap, reverse=True)
# Example: Find top 100 URLs with epsilon=0.001, delta=0.01
# Space: ~53 KB for CMS + ~3 KB for heap = 56 KB
# vs HashMap tracking all URLs: potentially GBs
Rate Limiting with CMS
7. HyperLogLog
Probabilistic algorithm for estimating cardinality (count of distinct elements) with remarkable space efficiency: 12 KB for 0.8% error on billions of items.
Core Intuition
- 50% have pattern 1xxxxxxx... (leading 0)
- 25% have pattern 01xxxxxx... (two leading 0s)
- 12.5% have pattern 001xxxxx... (three leading 0s)
- 6.25% have pattern 0001xxxx... (four leading 0s)
If max leading zeros seen = ρ:
Estimated cardinality ≈ 2^ρ
Problem: High variance (factor of 2 error)
Solution: Use m buckets, average their estimates
Algorithm
of 2^M j for all buckets] J --> K[Apply correction factors] K --> L[Return estimate]
Implementation
class HyperLogLog:
def __init__(self, precision=14):
self.p = precision # Number of bits for bucket index
self.m = 1 << precision # Number of buckets (2^p)
self.M = [0] * self.m # Max leading zeros per bucket
# Alpha constant for bias correction
if self.m >= 128:
self.alpha = 0.7213 / (1 + 1.079 / self.m)
elif self.m >= 64:
self.alpha = 0.709
elif self.m >= 32:
self.alpha = 0.697
elif self.m >= 16:
self.alpha = 0.673
def add(self, item):
# Hash to 64 bits
h = hash64(item)
# First p bits determine bucket
j = h & ((1 << self.p) - 1)
# Remaining bits: count leading zeros + 1
w = h >> self.p
rho = leading_zeros(w) + 1
# Update max for this bucket
self.M[j] = max(self.M[j], rho)
def cardinality(self):
# Harmonic mean
raw_estimate = self.alpha * (self.m ** 2) / sum(2 ** (-x) for x in self.M)
# Small range correction
if raw_estimate <= 2.5 * self.m:
zeros = self.M.count(0)
if zeros != 0:
return self.m * math.log(self.m / zeros)
# Large range correction
if raw_estimate > (1/30) * (2 ** 32):
return -2 ** 32 * math.log(1 - raw_estimate / (2 ** 32))
return raw_estimate
Precision vs Space Tradeoff
| Precision (p) | Buckets (m=2^p) | Space (6-bit registers) | Standard Error | Relative Error |
|---|---|---|---|---|
| 10 | 1,024 | 768 bytes | 1.04 / √m = 3.2% | ±6.4% at 95% confidence |
| 12 | 4,096 | 3 KB | 1.6% | ±3.2% |
| 14 | 16,384 | 12 KB | 0.8% | ±1.6% |
| 16 | 65,536 | 48 KB | 0.4% | ±0.8% |
| 18 | 262,144 | 192 KB | 0.2% | ±0.4% |
Merging HyperLogLogs
def merge(hll1, hll2):
# HLLs must have same precision
assert hll1.p == hll2.p
result = HyperLogLog(hll1.p)
for i in range(hll1.m):
result.M[i] = max(hll1.M[i], hll2.M[i])
return result
# Example: Union cardinality across shards
shard_hlls = [shard.get_hll() for shard in shards]
global_hll = reduce(merge, shard_hlls)
total_uniques = global_hll.cardinality()
8. HyperLogLog Applications
| Application | Metric | Why HLL | Space Savings |
|---|---|---|---|
| Web Analytics | Daily unique visitors | 0.8% error acceptable for dashboards | 12 KB vs 8 GB for 1B users |
| Database Statistics | Distinct values in column (for query optimization) | Fast cardinality estimates | Constant space per column |
| Network Monitoring | Unique source IPs per minute | Real-time processing | 12 KB per time window |
| Social Networks | Unique viewers of post | Mergeable across datacenters | 12 KB per post vs storing all viewer IDs |
| Advertising | Unique users reached by campaign | Union/intersection of audiences | 12 KB per campaign segment |
| Log Analysis | Distinct error messages | Space-efficient aggregation | 12 KB per log stream |
Redis HyperLogLog Commands
# Add elements
PFADD unique_visitors user:123 user:456 user:789
# Get cardinality estimate
PFCOUNT unique_visitors
# Returns: 3
# Merge multiple HLLs
PFADD page:home user:123 user:456
PFADD page:about user:456 user:789
PFMERGE total_visitors page:home page:about
PFCOUNT total_visitors
# Returns: 3 (union of both sets)
# Memory usage
MEMORY USAGE unique_visitors
# Returns: ~12KB regardless of cardinality
Multi-Dimensional Cardinality
# Track unique visitors per (country, device) dimension
# Without HLL: Need HashSet per dimension combo = 200 countries × 10 devices = 2000 sets
# With HLL: 12 KB × 2000 = 24 MB for all dimensions
for event in stream:
key = f"uniques:{event.country}:{event.device}:{date}"
redis.pfadd(key, event.user_id)
# Query: Unique mobile users in US on 2026-05-12
count = redis.pfcount("uniques:US:mobile:2026-05-12")
# Query: Unique users across all devices in France
keys = [f"uniques:FR:{device}:{date}" for device in devices]
redis.pfmerge("temp:query", *keys)
count = redis.pfcount("temp:query")
Real-World Performance
| Company | Use Case | Scale | Result |
|---|---|---|---|
| Unique post views | 100M+ posts | 1.2 TB → 1.2 GB (1000x reduction) | |
| Unique tweet impressions | 500M tweets/day | Real-time cardinality with 0.8% error | |
| Aggregate Knowledge | Ad audience measurement | Billions of users | 99.97% accuracy vs ground truth |
9. t-digest
Probabilistic data structure for estimating percentiles and quantiles from streaming data with bounded error and mergeability for distributed systems.
Core Concept
- Group nearby values into centroids
- Store (mean, weight) for each centroid
- More centroids near 0/100 percentile (tails)
- Fewer centroids near 50 percentile (median)
Space: O(compression) centroids
Typical: δ=100 → ~100-200 centroids → 2-4 KB
Why Non-Uniform Clustering
| Percentile Range | Importance | Centroid Density | Reason |
|---|---|---|---|
| 0-1%, 99-100% | Critical (SLA, outliers) | High | Need accuracy for p99, p99.9 |
| 1-10%, 90-99% | Important (tails) | Medium-High | Tail latency matters |
| 40-60% | Less critical (median) | Low | Few outliers near median |
Implementation Sketch
class TDigest:
def __init__(self, compression=100):
self.compression = compression # δ parameter
self.centroids = [] # List of (mean, weight)
self.count = 0
def add(self, value, weight=1):
self.count += weight
# Find nearest centroid
nearest = self._find_nearest(value)
# Compute capacity using scaling function
q = self._quantile_of_centroid(nearest)
capacity = self._capacity_at_quantile(q)
if nearest.weight + weight <= capacity:
# Merge into existing centroid
nearest.merge(value, weight)
else:
# Create new centroid
self.centroids.append(Centroid(value, weight))
# Periodically compress
if len(self.centroids) > self.compression * 3:
self._compress()
def quantile(self, q):
# q in [0, 1]
target = q * self.count
sum_weight = 0
for i, c in enumerate(self.centroids):
sum_weight += c.weight
if sum_weight >= target:
# Interpolate between centroids
if i == 0:
return c.mean
prev = self.centroids[i-1]
delta = (target - (sum_weight - c.weight)) / c.weight
return prev.mean + delta * (c.mean - prev.mean)
return self.centroids[-1].mean
def _capacity_at_quantile(self, q):
# Scaling function k(q,δ) determines centroid capacity
# More capacity near median, less at tails
return 4 * self.count * q * (1 - q) / self.compression
def merge(self, other):
# Combine centroids from both digests
self.centroids.extend(other.centroids)
self.count += other.count
self._compress()
Error Guarantees
| Percentile | Max Error (δ=100) | Max Error (δ=200) | Example (1M latencies) |
|---|---|---|---|
| p50 (median) | ±1% | ±0.5% | If true p50=100ms, estimate 99-101ms |
| p90 | ±0.5% | ±0.25% | If true p90=200ms, estimate 199-201ms |
| p99 | ±0.1% | ±0.05% | If true p99=500ms, estimate 499.5-500.5ms |
| p99.9 | ±0.01% | ±0.005% | If true p99.9=1000ms, estimate 999.9-1000.1ms |
Applications
| System | Metric | Compression (δ) | Space |
|---|---|---|---|
| Prometheus | API latency percentiles | 100 | ~2 KB per metric |
| ElasticSearch | Aggregation percentiles | 100 | Distributed merge across shards |
| PostgreSQL (pg_stats) | Column value distribution | 200 | Query planner estimates |
| DataDog | Custom metrics percentiles | 100 | Real-time dashboard |
10. Cuckoo Filter
Probabilistic data structure for set membership that supports deletion, offers better space efficiency than Bloom filters at low false positive rates, and provides faster lookups.
Key Differences from Bloom Filter
| Feature | Bloom Filter | Cuckoo Filter |
|---|---|---|
| Storage | Bit array | Array of buckets with fingerprints |
| Deletion | Not supported (or 4x space for counting) | Supported natively |
| Space at 1% FPR | 9.6 bits/item | 12.5 bits/item |
| Space at 0.1% FPR | 14.4 bits/item | 13.5 bits/item (better) |
| Lookup Speed | k hash computations | 2 bucket lookups (faster) |
| Insertion Failure | Never (always succeeds) | Possible if table full (rare) |
Structure
Algorithm
class CuckooFilter:
def __init__(self, capacity, bucket_size=4, fingerprint_size=8):
self.m = capacity // bucket_size # Number of buckets
self.b = bucket_size # Entries per bucket
self.f = fingerprint_size # Fingerprint bits
self.buckets = [[None]*self.b for _ in range(self.m)]
def _fingerprint(self, item):
return hash(item) % (1 << self.f)
def _index(self, item):
return hash(item) % self.m
def _alt_index(self, index, fingerprint):
return (index ^ hash(fingerprint)) % self.m
def insert(self, item):
f = self._fingerprint(item)
i1 = self._index(item)
i2 = self._alt_index(i1, f)
# Try to insert in i1 or i2
if self._insert_to_bucket(i1, f):
return True
if self._insert_to_bucket(i2, f):
return True
# Both buckets full: relocate existing items (cuckoo hashing)
i = random.choice([i1, i2])
for _ in range(500): # Max kicks
f, self.buckets[i][random.randint(0, self.b-1)] = \
self.buckets[i][random.randint(0, self.b-1)], f
i = self._alt_index(i, f)
if self._insert_to_bucket(i, f):
return True
return False # Filter full
def contains(self, item):
f = self._fingerprint(item)
i1 = self._index(item)
i2 = self._alt_index(i1, f)
return f in self.buckets[i1] or f in self.buckets[i2]
def delete(self, item):
f = self._fingerprint(item)
i1 = self._index(item)
i2 = self._alt_index(i1, f)
if f in self.buckets[i1]:
self.buckets[i1].remove(f)
return True
if f in self.buckets[i2]:
self.buckets[i2].remove(f)
return True
return False
Performance Comparison
| Metric | Bloom (1% FPR) | Cuckoo (1% FPR) | Bloom (0.1% FPR) | Cuckoo (0.1% FPR) |
|---|---|---|---|---|
| Bits per item | 9.6 | 12.5 | 14.4 | 13.5 (better) |
| Lookup cost | 7 hashes | 2 buckets | 10 hashes | 2 buckets (better) |
| Cache misses | 7 random locations | 2 buckets (better) | 10 random locations | 2 buckets (better) |
| Deletion | Not supported | O(1) | 4x space overhead | O(1) (better) |
| Load factor | Arbitrary | ~95% max | Arbitrary | ~95% max |
When to Use Cuckoo Filter
- Need deletion (e.g., cache eviction)
- Low FPR required (< 1%)
- Lookup speed critical
- CPU cache efficiency matters
When to Use Bloom Filter
- Write-once, no deletion needed
- Higher FPR acceptable (1-5%)
- Simpler implementation preferred
- Slightly better space at high FPR
11. Comparison Matrix
| Structure | Question Answered | Space Complexity | Error Type | Deletion | Mergeable |
|---|---|---|---|---|---|
| Bloom Filter | Is x in set? | O(1) per item ~10 bits/item |
False positives No false negatives |
No | Yes (OR bits) |
| Counting Bloom | Is x in set? | O(1) per item ~40 bits/item |
False positives False negatives (overflow) |
Yes | Yes (sum counters) |
| Count-Min Sketch | How many x? | O(1/ε × log 1/δ) ~5 KB typical |
Over-estimates Never under-estimates |
Yes (decrement) | Yes (sum arrays) |
| HyperLogLog | How many distinct x? | O(log log n) 12 KB typical |
±1.6% at 95% conf | No | Yes (max registers) |
| t-digest | What is pXX? | O(compression) 2-4 KB typical |
Bounded by compression | No (streaming) | Yes (merge centroids) |
| Cuckoo Filter | Is x in set? | O(1) per item ~13 bits/item |
False positives No false negatives |
Yes | No (complex) |
Detailed Feature Matrix
| Structure | Insert Time | Query Time | Memory | Accuracy Tunable | Best For |
|---|---|---|---|---|---|
| Bloom | O(k) | O(k) | Excellent | Yes (k, m) | Cache filters, deduplication |
| Counting Bloom | O(k) | O(k) | Good (4x Bloom) | Yes (k, m) | Dynamic sets with eviction |
| Count-Min | O(d) | O(d) | Excellent | Yes (ε, δ) | Heavy hitters, rate limiting |
| HyperLogLog | O(1) | O(m) | Exceptional | Yes (precision) | Cardinality, unique counts |
| t-digest | O(log c) | O(c) | Excellent | Yes (compression) | Percentiles, quantiles |
| Cuckoo | O(1) amortized | O(1) | Very Good | Yes (bucket, fingerprint) | Low-FPR membership + deletion |
Error Characteristics
| Structure | False Positives | False Negatives | Magnitude Error | Tail Accuracy |
|---|---|---|---|---|
| Bloom | Yes (configurable) | Never | N/A | N/A |
| Count-Min | N/A | N/A | Only over-estimates | N/A |
| HyperLogLog | N/A | N/A | ±1.6% (unbiased) | N/A |
| t-digest | N/A | N/A | Bounded by δ | Better at extremes |
| Cuckoo | Yes (configurable) | Never | N/A | N/A |
12. When to Use
Decision Tree by Problem Type
| Problem | Scale | Recommended Structure | Alternative | Reason |
|---|---|---|---|---|
| Check if URL seen before | Billions | Bloom Filter | Cuckoo (if need deletion) | Simple, space-efficient |
| Cache negative lookups | Millions | Bloom Filter | Cuckoo (lower FPR) | Avoid expensive DB queries |
| Track top-100 URLs | Billions | Count-Min Sketch | - | Heavy hitters with minimal space |
| Rate limiting per user | Millions | Count-Min Sketch | Token bucket | Over-estimation is safe |
| Count distinct visitors | Billions | HyperLogLog | - | Best space/accuracy tradeoff |
| Database cardinality stats | Millions | HyperLogLog | - | Fast query planner estimates |
| p99 latency monitoring | Billions | t-digest | Reservoir sampling | Better tail accuracy, mergeable |
| SLA breach detection (> p99.9) | Millions | t-digest | - | High accuracy at extremes |
| LRU cache with membership | Millions | Cuckoo Filter | Counting Bloom | Fast deletion support |
| Malicious IP blocking | Millions | Cuckoo Filter | Bloom | Need to remove IPs after timeout |
System Design Patterns
| Pattern | Structures Used | Architecture | Example |
|---|---|---|---|
| Cache Aside with Bloom | Bloom + Cache + DB | Check Bloom → Cache → DB | Avoid DB lookups for missing keys |
| Two-Level Frequency | Count-Min + HashMap | CMS estimates all, map stores top-K | Heavy hitter detection |
| Distributed Cardinality | HLL per shard + merge | Each shard maintains HLL, merge at query | Global unique visitors across DCs |
| Percentile Aggregation | t-digest per server + merge | Per-server digests merged for fleet view | Global p99 latency |
| Write-Through with Cuckoo | Cuckoo + Cache | Update both on write, check Cuckoo on read | Fast membership + eviction support |
Common Combinations
| Use Case | Combination | Why Both |
|---|---|---|
| Web Analytics Dashboard | HLL (uniques) + Count-Min (heavy hitters) + t-digest (latency) | Each answers different question with minimal space |
| Rate Limiter | Bloom (blocked users) + Count-Min (rate tracking) | Fast blocked check, then accurate count |
| Log Analysis | HLL (distinct errors) + Count-Min (error frequency) + Bloom (seen before) | Comprehensive stats with < 100 KB total |
| Network Monitoring | Cuckoo (active flows) + HLL (unique IPs) + t-digest (packet size distribution) | Track active flows with deletion + stats |
- Membership → Bloom/Cuckoo
- Frequency → Count-Min Sketch
- Cardinality → HyperLogLog
- Percentiles → t-digest
Implementation Checklist
| Consideration | Bloom/Cuckoo | Count-Min | HyperLogLog | t-digest |
|---|---|---|---|---|
| Choose Parameters | Set FPR, calculate m and k | Set ε and δ, calculate w and d | Set precision (12-16 typical) | Set compression (100-200) |
| Hash Functions | MurmurHash3, xxHash | Independent hashes or double hashing | 64-bit hash (MurmurHash3) | Any good hash |
| Serialization | Bit array to bytes | 2D array of ints | Array of bytes (registers) | List of (mean, weight) pairs |
| Distributed | OR bits across replicas | Sum arrays across shards | Max registers across DCs | Merge centroids from nodes |
| Monitoring | Track load factor (n/m) | Track collision rate | Track register distribution | Track centroid count |