Approximate data structures for huge scale
Open in the interactive app: practice, quiz, mock interview →In one minute
- At scale, the textbook structures (a hash set, a sorted array, a full scan) still give the right answer, but they cost too much memory, too much network or too much time per query.
- The structures in this section trade a little of something you can afford to lose, usually exactness, for orders of magnitude less space or time.
- Each one answers one question well: is this item present, how many distinct items, how often has this item appeared, how similar are these sets, what is near this point, what is the next key in order, or do these two copies match.
- Their errors are bounded and usually one-sided, which is what makes them safe to build on: a Bloom filter never says "absent" for something present, and a count-min sketch never undercounts.
- Many of them are mergeable, so shards and time windows can be summarised separately and combined later.
How it works
The question decides the structure
Before reaching for a clever structure, write down the exact question, over how many items, and how wrong the answer may be. Teams skip the last part. "Unique visitors today, within 1%?" is 12 KB of HyperLogLog registers in Redis (Redis docs). "Exactly how many?" may need gigabytes.
| Question | Structure | Memory for a typical setting | Error | Lesson |
|---|---|---|---|---|
| Have I seen this key? | Bloom filter | About 1.2 MB for 1 million keys at 1% false positives | False positives only | Bloom Filters |
| Have I seen it, and can I delete it? | Cuckoo filter | Similar to Bloom at low false-positive rates | False positives only | Cuckoo Filter |
| How many distinct items? | HyperLogLog | 12 KB for about 0.8% standard error, at any count | Relative, two-sided | HyperLogLog |
| How often has this item appeared? | Count-min sketch | About 54 KB for 0.1% of total count error at 99% confidence | Overestimates only | Count-Min Sketch |
| How similar are two sets? | MinHash | A few hundred integers per set | Shrinks with signature length | MinHash |
| What is near this point? | Geohash, quadtree, R-tree, S2 or H3 | An index over the points | Exact after a filter step | Geohash, Quad Trees, R-Trees, S2 and H3 |
| What comes next in key order? | Skip list | About 1.33 pointers per key at p = 1/4 | Exact | Skip Lists |
| Do these two replicas match? | Merkle tree | One hash per node | Exact, with negligible collision risk | Merkle Trees |
One newer family is worth knowing even though it has no lesson of its own. When a set is built once and never changes, such as the keys in an immutable file, static filters beat both Bloom and cuckoo on space. Xor filters sit within 23% of the theoretical minimum, binary fuse filters within 13%, against 44% for a Bloom filter (Graf and Lemire). Ribbon filters make the same trade, and RocksDB ships them (RocksDB wiki).
Two databases-section lessons belong to the same family: LSM Trees, which make writes sequential and lean on Bloom filters to skip files, and B-Trees and B+ Trees, which shape an index to the disk page. Consistent Hashing applies the same thinking to placing keys on machines.
Three tricks underneath
Nearly every structure here combines three ideas.
Hashing to spread items uniformly. A good hash turns any key into random-looking bits, so a fixed-size array can summarise an unbounded stream: Bloom filters set hashed bits, HyperLogLog picks a register and counts leading zeros, count-min increments hashed counters.
Accepting collisions and bounding their effect. Two items may land on the same bit or counter. The structure is designed so collisions push the error in one known direction (Bloom: a false "maybe present"; count-min: a count that is too high) and the size parameter controls how often.
Summaries that merge. Bloom filters with the same parameters combine with bitwise OR, HyperLogLogs by register-wise maximum, count-min sketches by adding counters. Each shard or hour builds its own summary, and a query merges them without touching raw data.
The spatial and ordered structures use hierarchical subdivision instead: split space or key order into levels so a query descends a few levels rather than scanning. Geohash and S2 turn two dimensions into a one-dimensional key that a B-tree can index.
Recognising the problem shape
This section is the "finding things fast" and "trust but verify" shapes at large scale. The membership question appears in a crawler checking if a URL was fetched and in a password service checking a breached-password list. The cardinality question appears in analytics, in a query planner estimating distinct values, and in counting distinct source IPs during an attack.
Down the stack
Layer 1The contract
Each probabilistic structure promises an error bound as a function of its size, and the bound is precise enough to test. A Bloom filter with m bits, n items and k hash functions has a false-positive rate near (1 - e^(-kn/m))^k, minimised at k = (m/n) ln 2, giving about 9.6 bits per key for 1% and 14.4 for 0.1% (Broder and Mitzenmacher). It never returns a false negative, unless items are deleted, which a plain Bloom filter does not support. HyperLogLog with m registers has a relative standard error of about 1.04 / sqrt(m), so 16,384 registers give 0.81% whether the true count is ten thousand or ten billion (Flajolet et al.). A count-min sketch with width ceil(e / eps) and depth ceil(ln(1 / delta)) overestimates any item's count by at most eps times the total count, with probability at least 1 - delta (Cormode and Muthukrishnan).
These bounds assume random hashing, not adversaries; untrusted input needs seeded or keyed hashes.
Layer 2The mechanism
The concrete layouts are small and specific. Redis stores a HyperLogLog as 16,384 six-bit registers, 12,288 bytes in the dense encoding, and uses a sparse run-length encoding while most registers are still zero; PFMERGE takes the per-register maximum (antirez). BigQuery exposes HyperLogLog++ sketches directly: HLL_COUNT.INIT takes a precision from 10 to 24, default 15, and the sketches can be stored and merged later, which plain APPROX_COUNT_DISTINCT cannot do (BigQuery docs). RocksDB attaches a filter to each SST file; the Bloom implementation introduced with format_version=5 (RocksDB 6.6) confines each key's probes to a single 64-byte cache line, and the Ribbon filter added in 6.15 saves about 30% of filter memory for 3 to 4 times the build CPU (RocksDB wiki). Cassandra keeps a Bloom filter per SSTable, off-heap, and lets you set bloom_filter_fp_chance per table: 0.01 by default, 0.1 under leveled compaction (Cassandra docs). Redis sorted sets use a skip list with span counters so rank queries are logarithmic. Git and Certificate Transparency logs are Merkle trees: change one byte in one file and every hash up to the root changes.
Layer 3The machine
These structures win because of the memory hierarchy. An L1 cache hit costs about 1 ns, L2 about 4 ns, a main memory reference about 100 ns. A 12 KB HyperLogLog fits in a 32 or 48 KB L1 data cache, so updating it is a hash plus one cache-resident write. A plain Bloom filter with k = 7 touches up to 7 random cache lines, about 700 ns if they miss, which is why blocked Bloom filters keep all probes in one 64-byte line for a small rise in false positives.
Exact structures pay per-entry overhead that dwarfs the key. A 64-bit ID is 8 bytes, but a Java HashSet<Long> spends around 50 bytes per entry on the boxed Long, hash node and table slot. A billion distinct IDs is tens of gigabytes; the HyperLogLog is still 12 KB. The network shows the same gap: merging HyperLogLogs from 200 shards moves 2.4 MB, while merging exact sets moves the data.
Why it's built this way
- Memory is fast but small, and the data is huge. So keep a compact summary in memory and the full data elsewhere.
- Exact answers need state proportional to the data. So for questions that tolerate error, trade exactness for state proportional to the accuracy wanted.
- Unbounded error is useless. So choose structures whose error is bounded, tunable by size, and ideally one-sided, so a cheap "no" can skip expensive work safely.
- Data is spread across machines and time. So prefer summaries that merge by OR, max or sum, and compute global answers from partial ones.
Diagram
flowchart TD
Q["What must the system answer?"] --> M{"Membership?"}
M -->|yes| X{"Exact set fits budget?"}
X -->|yes| E["Hash set or index"]
X -->|no| B["Bloom or cuckoo filter"]
M -->|no| C{"Count distinct?"}
C -->|yes| H["HyperLogLog"]
C -->|no| F{"Frequency per item?"}
F -->|yes| K["Count-min sketch"]
F -->|no| S{"Near a point?"}
S -->|yes| G["Geohash, quadtree, R-tree, S2, H3"]
S -->|no| O["Skip list, Merkle tree, MinHash"]Worked example
An ad platform reports daily unique users per campaign for 50,000 campaigns, plus weekly uniques that are not the sum of daily ones because the same user appears on several days.
Exact. Campaigns average 400,000 unique users a day. A hash set of 64-bit IDs at about 40 bytes per entry in a compact native implementation needs 50,000 × 400,000 × 40 bytes, which is 800 GB per day, and weekly uniques need the union of seven sets per campaign.
HyperLogLog. At precision 14, each campaign-day is 12 KB: 50,000 × 12 KB is 600 MB per day, with 0.81% standard error. A weekly figure is the register-wise maximum of seven daily sketches, still 12 KB and still 0.81%. Ninety days of history is 54 GB, small enough for an ordinary database column.
When to stay exact. Billing charges per unique user and must reconcile. The team keeps HyperLogLog for dashboards and computes exact uniques for invoiced campaigns in a nightly batch over raw logs: approximate for interactive questions, exact for money.
A second question. The platform must not show a user the same ad twice a day. A Bloom filter over user:ad pairs costs 9.6 bits per pair at 1% false positives. A false positive wrongly skips an ad, costing a little revenue but never annoying the user, so the one-sided error points the right way. The practice task turns this reasoning into a sizing tool.
In the wild
- Mozilla, 2025: certificate revocation for every browser in about 300 kB a day. Firefox 137 turned on CRLite for desktop users. It pushes the revocation status of every certificate as a compact set-membership filter, so the browser no longer makes a blocking OCSP call, which cost about 100 ms at the median. The first design used multi-level cascades of Bloom filters, whose bandwidth was too high; the shipped version uses a partitioned two-level cascade of Ribbon filters. As CRLite took over 80% of checks, median TLS handshake time fell from 56.4 ms to 39.9 ms (Mozilla Hacks).
- Ethereum, 2024: a Bloom filter that outgrew its size. Every Ethereum block header carries a fixed 256-byte logs Bloom filter (consensus specs), sized when blocks held far fewer logs. EIP-7668 proposes emptying it because, at current gas limits, its false-positive rate is so high that filtering saves little, and most apps already query history through off-chain indexers (EIP-7668). It is the "sized for today's count" pitfall at protocol scale, where the size cannot change without a hard fork.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Exact hash set or index | No error, supports any later query | Memory grows with data, plus per-entry overhead | Data fits, or answers must reconcile exactly |
| Bloom or cuckoo filter | Bits per key, one-sided error, fast | No listing, plain Bloom cannot delete | Cheap "definitely not" in front of expensive lookups |
| HyperLogLog | Fixed tiny size, mergeable | Only counts, cannot list items | Distinct counts across shards and windows |
| Count-min sketch | Fixed size, mergeable, never undercounts | Overestimates rare items under heavy load | Heavy hitters and rate tracking |
| Sampling | Simple, supports many queries | Misses rare items; error depends on sample | Exploratory analytics on huge datasets |
Pitfalls in production
- No error budget written down. A stakeholder compares a sketch to an exact report and files a bug. Agree the tolerated error first, and label approximate numbers.
- Sizing for today's count. A Bloom filter sized for 10 million keys that receives 50 million has a false-positive rate above 80%. Size for growth, monitor the fill ratio, or use a scalable variant that adds layers.
- Merging incompatible summaries. HyperLogLogs with different precision or seed cannot merge. Version parameters with the data.
- Weak or attacker-controlled hashing. Predictable hashes let crafted inputs collide and skew counts or fill filters. Use a strong seeded hash for untrusted input.
- Sketches where exactness matters. Billing and blocking decisions need exact answers. Use sketches to narrow candidates, then confirm.
Leadership lens
- Ask every proposal for a sketch to state the question, the error bound, which direction the error goes, and who is harmed by a wrong answer.
- These structures can turn an 800 GB problem into 600 MB. Weigh that against the cost of explaining approximate numbers.
- Standardise on one library, such as Apache DataSketches or Redis's built-in types, so parameters and serialisation are consistent across teams. Check versions when you do: since Redis 8, Bloom, cuckoo, count-min, top-k and t-digest ship in the core server, and the separate RedisBloom module is no longer released (RedisBloom).
Check yourself
Why is a one-sided error more useful than a two-sided one of the same size?
Show answer
A one-sided structure gives one answer you can trust completely. A Bloom filter's "absent" is always right, so it can safely skip a disk read, and only the "maybe" answers pay for the exact check.
Why does HyperLogLog use the same memory for ten thousand and ten billion distinct items?
Show answer
It stores a fixed number of registers, each holding the longest run of leading zeros seen; the error depends on the register count, not on the number of items.
Two services each keep an hourly Bloom filter. How do you answer "seen in either hour" without the raw keys?
Show answer
If both filters use the same size and hash functions, OR them bit by bit; the result is the filter of the union. With different parameters you cannot merge them.
Why can't a count-min sketch tell you which items are the most frequent on its own?
Show answer
It only answers point queries for items you name. Finding heavy hitters needs a small heap of candidates updated as items stream past.
Sources and further study
Primary
- Bloom: Space/Time Trade-offs in Hash Coding with Allowable Errors (CACM, 1970): the original membership filter.
- Cormode and Muthukrishnan: An Improved Data Stream Summary, the Count-Min Sketch (LATIN 2004): the frequency sketch and its width and depth bounds.
- Graf and Lemire: Binary Fuse Filters (JEA, 2022): static filters within 13% of the space lower bound.
Systems docs
- BigQuery HyperLogLog++ functions: storable, mergeable cardinality sketches in a warehouse.
- Cassandra: Bloom filters: a per-table false-positive knob and its memory cost.
- RedisBloom README: which probabilistic types moved into core Redis 8.
Books
- Designing Data-Intensive Applications (1st ed.), Martin Kleppmann, Ch 3: Storage and Retrieval, where Bloom filters keep LSM reads cheap.
- Database Internals (1st ed.), Alex Petrov, Ch 7: Log-Structured Storage, implementation details of per-table filters.
Other curricula
- Stanford CS166: Data Structures: treats sketches, cuckoo hashing and cuckoo filters as hashing problems with provable error bounds.
Watch
- Data Structures for Big Data in Interviews (Hello Interview): where Bloom filters, count-min sketch and HyperLogLog fit in system design.
- Bloom Filters, Algorithms You Should Know #2 (ByteByteGo): a short visual explanation of the most common filter.
- HyperLogLog Hit Counter (Computerphile): the leading-zeros intuition behind cardinality estimation.
Free resources
- Redis probabilistic data types: production Bloom, cuckoo, HyperLogLog, count-min and top-k structures with sizing guidance.
- Apache DataSketches: a library of mergeable sketches with documented accuracy.
- Network Applications of Bloom Filters: A Survey (Broder and Mitzenmacher): the standard survey of Bloom filter uses and variants.
- HyperLogLog (Flajolet et al.): the original algorithm and its error analysis.
- RocksDB Bloom Filter (RocksDB wiki): bits per key and cache-local filter layouts in a real storage engine.
- Redis new data structure: the HyperLogLog (antirez): how Redis fits HyperLogLog into 12 KB with a sparse encoding.