System designDeep dive9 min
HyperLogLog: counting distinct things
Counting distinct visitors exactly means remembering every one of them. HyperLogLog counts them in 12 KB, whether there are a thousand or a billion, to within about 1%, and two counts can be merged without counting anyone twice. It works by watching for rare patterns in hashes.
1 · The problem
Counting distinct things exactly means remembering every one
A news site wants to show "unique readers" for each of its 20,000 articles, per hour, per day and per week, and the same for the whole site, which has about 40 million readers a day. Counting page views is easy: add one per view. Counting distinct readers isn't, because a reader who comes back must not be counted again.
The exact way is a set: keep every reader ID seen, and count the set. That has two costs:
- Memory grows with the answer. 40 million 8-byte reader IDs is 320 MB for the site's daily count alone, before any set overhead. One set per article per hour multiplies that by thousands.
- Counts don't add up. If 600,000 people read in the morning and 600,000 in the evening, the day's total is somewhere between 600,000 and 1.2 million. Only the sets themselves can tell, so every set must be kept to answer any longer period.
HyperLogLog gives up exactness to fix both. It estimates the number of distinct items, the cardinality, in a fixed amount of memory, typically 12 KB, with an error of about 1%. And two of its sketches can be merged into the sketch of the combined set, so a day can be built from its hours.
Aspect | Exact set | HyperLogLog |
|---|---|---|
Memory for 40 million daily readers | 320 MB of IDs, plus set overhead | About 12 KB |
Error | None | About 1% |
Morning plus evening | Keep both sets and merge them | Merge the two sketches |
Grows with the number of readers | Yes | No |
2 · The intuition
A rare pattern in the hashes tells you how many you have seen
Hash each reader ID to 64 random-looking bits. The same reader always gives the same hash, so repeats change nothing. Then look at how many zeros each hash starts with. Each bit is 0 half the time, so:
Hash starts with | Chance for one hash | Distinct items you'd typically need to see it |
|---|---|---|
1 | 1 in 2 | About 2 |
0001 (3 zeros) | 1 in 16 | About 16 |
10 zeros, then a 1 | 1 in 2,048 | About 2,000 |
20 zeros, then a 1 | 1 in about 2 million | About 2 million |
So remembering just one number, the longest run of leading zeros seen so far, gives a rough count. If the longest run among all hashes is 20 zeros, the site has probably seen a million or two distinct readers. It is like someone who reports that their longest streak of heads today was 13: they probably flipped a coin thousands of times.
One observation is far too noisy, though. A single lucky hash with 20 zeros among the first ten readers makes the estimate a million. The answer is to make many independent observations and average them.
3 · The registers
Split the stream into thousands of small estimates and average them
HyperLogLog splits the readers into m groups using the hash itself, and keeps one small counter, a register, per group. In Redis's version, m = 16,384:
Each register records the longest run of leading zeros, plus one, among the hashes routed to it. A reader seen again lands on the same register with the same value and changes nothing.
To estimate, HyperLogLog combines the registers with a harmonic mean, which keeps one register with an unusually long run from dragging the answer up. The formula is E = α × m² ÷ Σ 2^(−M[j]), where M[j] is register j and α ≈ 0.7213 corrects a known bias. The relative error is about 1.04 ÷ √m: for 16,384 registers, 1.04 ÷ 128 ≈ 0.81%.
Each register only has to hold a number up to about 50, so 6 bits are enough. Memory is set by m alone, never by how many readers there are:
Registers (m) | Typical error | Memory, 6-bit registers |
|---|---|---|
1,024 | 3.25% | 768 bytes |
4,096 | 1.63% | 3 KB |
16,384 | 0.81% | 12 KB |
65,536 | 0.41% | 48 KB |
Four times the memory halves the error, since it falls with the square root of m. A 16,384-register sketch, run over growing numbers of distinct reader IDs, gave these errors:
Estimates of 1,003; 100,050; 1,009,961 and 5,023,053 from the same 12 KB. The standard error is 0.81%, so a single estimate can easily land 1% off, in either direction.
4 · The merge
Two sketches merge by taking the larger of each register
Here is the property that makes HyperLogLog more than a memory saving. A reader always lands in the same register with the same value, so the sketch of two combined sets is just the register-by-register maximum of their sketches. The merged sketch is exactly the one you would have built by feeding both streams into a single sketch: no reader is counted twice.
Sketch | Estimate | True count |
|---|---|---|
Morning readers | 610,591 | 600,000 |
Evening readers | 602,886 | 600,000 |
Adding the two counts | 1,213,477 | Wrong: 200,000 read both |
Merging the two sketches | 1,009,961 | 1,000,000 |
So the news site never keeps raw reader lists. It keeps one sketch per article per hour, and merges them on demand into days, weeks or any group of articles. In Redis that is three commands:
Unique readers in Redis
PFADD readers:article:7713:2026-10-02T09 reader-48213377
# The day: merge the 24 hourly sketches into a new one
PFMERGE readers:article:7713:2026-10-02 readers:article:7713:2026-10-02T00 ... readers:article:7713:2026-10-02T23
PFCOUNT readers:article:7713:2026-10-02PFCOUNT with several keys merges on the fly instead, which is slower and its result isn't cached, so a busy page should read a stored merged sketch. Warehouses offer the same: BigQuery's HLL_COUNT.INIT builds sketches in a query, HLL_COUNT.MERGE combines them, and a rollup table can store sketches rather than counts so later queries can still combine any periods (Data warehouses, lakes & pipelines).
5 · The limits
Unions are exact to merge, but intersections and small counts are not
The editors ask a new question: how many readers read both article A and article B? Each had about a million readers. There is no merge for an intersection, so the obvious trick is the inclusion-exclusion rule: |A ∩ B| = |A| + |B| − |A ∪ B|.
1,009,961 + 999,930 − 2,000,450 = 9,441, almost double the truth. Each of the three estimates is off by thousands, and subtracting large approximate numbers leaves only their errors.
The error of a union of 2 million is about 0.81%, roughly 16,000 readers, more than three times the answer. Small overlaps between large sets can't be estimated this way. HyperLogLog is a poor fit when you need intersections or differences with reasonable accuracy; other sketch families, such as Theta sketches, do that better.
Small counts need care for a different reason. With few items, most registers are still zero and the raw formula is biased. Implementations switch methods at the low end:
Technique | What it does |
|---|---|
Count empty registers | The original algorithm estimates m × ln(m ÷ V) while V registers are still zero, which is accurate for small sets |
Stay sparse | Store only registers that are set, at higher precision, until the list outgrows the array; Redis then goes dense (12,288 bytes) |
Correct the bias | Google's HyperLogLog++ adds an empirical correction for the middle range, and its 64-bit hash means very large counts need no special correction |
And a sketch can only grow. It can't remove a reader, list who was counted, or tell whether one particular reader was seen; for that last question, see Bloom filters.
6 · In practice
Use it when about 1% is fine and the counts must combine
Need | Use |
|---|---|
Unique visitors, searches, devices, IPs at scale | HyperLogLog |
Counts over many periods or groups that must combine | HyperLogLog sketches, stored and merged |
Overlaps between groups | Other sketches built for set operations, or exact sets |
Billing, quotas, anything legally exact | Exact counts |
Small sets, a few thousand items | An exact set is cheap enough |
The algorithm counts beyond a billion items with about 2% error in only 1.5 KB. Error is a design choice, not an accident: pick m for the error you can live with, and the memory follows. BigQuery, for example, lets a sketch's precision be set from 10 to 24, defaulting to 15; higher means more accurate and larger.
For the news site, the checklist:
- One sketch per article per hour, merged into longer periods and wider groups on demand.
- Store sketches, not counts, in rollups, so any combination stays possible.
- Use the same hash and precision everywhere; sketches only merge with sketches built the same way.
- Show estimates as estimates ("about 1.2 million readers"), never as exact figures.
- Don't subtract sketches to find overlaps between large groups.
Check yourself
4 questions
Takeaways
Remember this
- Counting distinct items exactly needs memory proportional to the count, and exact counts for separate periods can't be added.
- HyperLogLog hashes each item, routes it to one of m registers, and keeps the longest run of leading zeros seen in each.
- Its error is about 1.04 ÷ √m, independent of the count: 16,384 six-bit registers give about 0.81% in 12 KB.
- Sketches merge losslessly by taking each register's maximum, so store hourly sketches and build any longer period from them.
- It handles unions, not intersections; subtracting estimates of large sets drowns small overlaps in error.