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:

reader-4821337764-bit hashFirst 14 bitspick register 9,301Other 50 bits3 zeros, then a 1: 4Register 9,301keeps the max seenwhich registermax(old, 4)

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:

1,000 readers
0.31 percent too high
100,000 readers
0.05 percent too high
1,000,000 readers
1 percent too high
5,000,000 readers
0.46 percent too high

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-02

PFCOUNT 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|.

True overlap
5,000 readers
Estimated overlap
9,441 readers

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

1. The morning sketch estimates 610,591 readers and the evening sketch 602,886. What is the best estimate of the day's unique readers?
2. A sketch uses 16,384 registers. How much memory does it need to count 40 million readers rather than 40 thousand?
3. To halve HyperLogLog's typical error, how must the number of registers change?
4. Two articles each have about a million readers. Why is subtracting the union's estimate from the two estimates a poor way to count readers of both?

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.