HyperLogLog
From "Is It In?" to "How Many?"
The last two lessons — Bloom Filters and Cuckoo Filters — answered one question: is this thing in the set? They test membership. This one answers a completely different question that comes up just as often: how many distinct things have I seen? Unique visitors to a site today. Distinct IP addresses hitting a firewall. Distinct search terms this hour. Distinct users who saw an ad.
The exact way is brutal. To count distinct, you keep a set — every new item you check "have I seen this?" and add it if not. That works, and for a few thousand items it's the right answer. But the memory grows with the number of distinct items: counting a billion distinct IPs means holding a billion IPs in RAM — tens of gigabytes, per counter, and you often want thousands of counters (per page, per hour, per campaign). Exact distinct counting simply doesn't fit.
HyperLogLog answers "how many distinct?" using a fixed, tiny amount of memory — about 12 kilobytes — no matter whether the true count is a thousand or a billion, with a typical error under 1%. And it does it while storing not one of the items. That sounds impossible; counting distinct things is supposed to mean remembering which things you've seen. The trick is to stop remembering the things and instead watch how rare a pattern the stream has accidentally produced — because rarity, it turns out, is a measuring stick for cardinality.

Rarity Is a Ruler
Here's the whole idea in one image: flip a coin until you get heads, and count the tails first. Usually you get heads quickly. But if you told me the longest opening run of tails you ever saw in a big batch of tries was ten, I'd bet you ran that experiment about a thousand times — because a run of ten tails happens only about once in 2¹⁰ ≈ 1024 tries. The rarest thing you've observed is a ruler for how many times you tried.
HyperLogLog turns every item into exactly that experiment. Hash the item into a string of uniformly random-looking bits, and count its leading zeros. Because each bit is 50/50, the chance of seeing k leading zeros is 1 in 2ᵏ — one leading zero is a coin flip, three leading zeros is 1 in 8, ten is about 1 in 1,024. So across a whole stream of distinct items, the longest run of leading zeros you've seen estimates the count: if the rarest pattern is k leading zeros, you've probably fed in about 2ᵏ distinct items. (Call that longest run the rank.)
And it genuinely works: hash a million distinct items, and the longest leading-zero rank 21 is about what shows up — and 2²¹ ≈ 2,000,000, the right order of magnitude. Duplicates don't fool it, either: re-hashing an item you've already seen produces the same bits and the same rank, so it can't push the maximum any higher. You're measuring distinctness, for free.
There's just one problem, and it's a big one: a single "longest run" is wildly noisy. One unlucky item that happens to hash to twenty leading zeros would scream "a million items!" after you've added ten. The raw estimate can be off by a factor of two in either direction. A single ruler isn't enough.

A Thousand Rulers: Stochastic Averaging
The fix is the heart of the algorithm, and it's beautiful: don't use one ruler — use thousands, and average them.
Split the stream into m independent buckets, called registers. Which register an item belongs to is decided by the item's own hash: take the first p bits as the register index (so m = 2ᵖ registers), and use the remaining bits for the leading-zero rank. Each register quietly keeps the maximum rank it has ever seen — its own little longest-run experiment, run on the roughly n/m items that landed in it. One lucky item now only corrupts one register out of thousands, instead of the whole estimate. This is stochastic averaging: the hash does double duty, both partitioning the stream and measuring each part.
Then you combine the m registers into one number. HyperLogLog's specific choice — the thing that makes it Hyper — is to combine them with a harmonic mean (roughly, α · m² / Σ 2^(−register), with a small constant α ≈ 0.7). Why the harmonic mean instead of a plain average? Because it's robust to large outliers: one register that got unlucky and holds a huge rank barely moves a harmonic mean, whereas it would wreck an arithmetic one. That single choice is what tamed the earlier "LogLog" algorithm into HyperLogLog.
The payoff is a clean, tunable law. The relative standard error is about 1.04 / √m — so accuracy is a dial you turn with the number of registers. A real run shows it converging exactly as promised: ~14% error at 16 registers, 3.7% at 256, 1.6% at 4,096, and ~0.81% at 16,384. Quadruple the registers, halve the error. The lab below lets you feel it directly — add items and watch each one light up a register and set a rank, then slide the register count up and watch a hopeless estimate snap into a sharp one.


The Budget: 12 KB Counts a Billion
Now count the cost, because this is where HyperLogLog stops being clever and starts being magic.
A register only ever holds a small number — the longest run of leading zeros, which for a 64-bit hash tops out around 50, so 6 bits per register is plenty. The standard configuration uses m = 2¹⁴ = 16,384 registers, which is 16,384 × 6 bits = 12,288 bytes ≈ 12 KB. Plug that m into the error law: 1.04 / √16384 = 0.81%. So twelve kilobytes buys you distinct-counting with under one percent error.
And here's the part that breaks intuition: that 12 KB is flat. It does not grow with the number of distinct items. A HyperLogLog that has counted a thousand distinct users and one that has counted a billion are both exactly 12 KB — the registers just hold slightly larger ranks. Against an exact set, the gap is absurd: at a billion distinct items, an exact set needs roughly 20 GB (tens of gigabytes), so the HyperLogLog is roughly 1.6 million times smaller. And because it counts distinctness, duplicates are free — you can pour two million repeated events into it and the estimate doesn't budge, because a repeat can only re-set a register to a rank it already holds. Constant memory, sublinear error, unlimited duplicates. That's the trade §8 has been building toward: give up exactness, and the impossible fits in a cache line's worth of RAM.

Merge for Free
There's one more property, and for distributed systems it might be the most important of all: HyperLogLogs merge.
Because a register just holds the maximum rank seen for the items that hashed to it, the union of two HyperLogLogs is computed by taking, register by register, the larger of the two values. That's it — a register-wise maximum. The merged sketch is exactly the sketch you'd have built by feeding both streams into one HyperLogLog from the start, and it counts shared items only once, automatically, because a duplicate across the two streams was always going to produce the same rank in the same register.
This is enormous in practice. You can count unique visitors per server, or per hour, or per shard, entirely independently — no coordination, no shuffling raw data around — and then combine any subset of those sketches with a cheap register-wise max to get the exact-same-quality distinct count over the whole. Weekly uniques? Merge the seven daily sketches. Uniques across the fleet? Merge one sketch per machine. A real merge bears this out: two sketches of 60,000 and 50,000 items that overlapped on 20,000 merged to an estimate of about 91,800 against a true union of 90,000 — the 20,000 overlap counted once, not twice. Try doing that by shipping and de-duplicating raw sets.
Where It Runs, and the Sharp Edges
HyperLogLog is everywhere you need to count uniques over data too big or too fast to store.
- Redis ships it as a first-class type:
PFADDto add,PFCOUNTto estimate,PFMERGEto union — and the commands are prefixed PF in memory of Philippe Flajolet, who invented the algorithm. A real session makes it concrete:PFADD-ing 100,000 distinct visitors and callingPFCOUNTreturned 99,471 (0.53% off), in a key thatSTRLENreports as 12,304 bytes; pouring in 500,000 duplicate adds left the count unchanged; andPFMERGE-ing a Monday and Tuesday sketch gave the two-day unique count directly, overlap and all. - Data warehouses lean on it hard: BigQuery's
APPROX_COUNT_DISTINCT, Presto/Trino'sapprox_distinct, Spark'sapprox_count_distinct, Druid, and Elasticsearch's cardinality aggregation are all HyperLogLog under the hood — becauseCOUNT(DISTINCT ...)exactly over billions of rows is a query that either runs out of memory or takes forever.
Two sharp edges worth knowing. First, HyperLogLog is weakest when the count is small — with only a handful of distinct items, most registers are still empty and the harmonic-mean formula misbehaves, so real implementations switch to a simpler linear-counting correction in that range (and HyperLogLog++, Google's 2013 refinement, adds 64-bit hashing, a compact sparse representation for small counts, and empirical bias correction). Second, it answers exactly one question — how many distinct — and only approximately. It cannot tell you whether a specific item is present (that's a Bloom filter), it cannot list the items, and if you need the exact number, it's the wrong tool. Reach for it when the set is huge, the memory is fixed, and a ~1% error on a distinct count is a price you're glad to pay.
The Counting Sketches Begin
Step back and the shape of §8's second half comes into focus. The membership sketches — Bloom and Cuckoo — answered is this one thing in the set? HyperLogLog opened the counting sketches by answering a question no set membership can: how many distinct things are in the set? — and it did it in a flat 12 KB by measuring rarity instead of remembering items.
Two counting questions remain, and each gets the same treatment: a clever hash-based structure that trades a little accuracy for enormous space savings. "How often does each thing occur?" — the heavy hitters, the frequency of every key in a stream — is answered by a Count-Min sketch, and it's next. "How similar are two sets?" — the overlap between two huge collections — is answered by MinHash. You've now seen the deepest of the counting sketches; the remaining two reuse the same instinct you just built — hash it, keep a tiny summary, accept a bounded error — pointed at frequency and similarity instead of cardinality.