HyperLogLog

Cardinality in a sketch

Prerequisites

graph TD
  Prob[Probability basics] --> Bloom[Bloom Filters]
  Hash[Hash functions] --> HLL[HyperLogLog]
  Bloom --> HLL
  Stream[Streaming / cardinality] --> HLL

When to use

  • Distinct-count analytics on massive streams (daily active users, unique IPs, ad impressions) where exact SET storage is too heavy.
  • Fixed memory budgets — Redis PFADD / PFCOUNT style structures stay ~12 KB regardless of how many uniques you insert.
  • Mergeable sketches across shards or time windows when you only need approximate cardinality, not per-key identity.

When not to use

  • You need exact distinct counts or must list which elements are unique (use a hash set or database).
  • The universe is tiny (dozens of keys) — len(set(stream)) is simpler and exact.
  • You must detect membership of a specific element (use a Bloom filter or hash map).

Lab

On the interactive HyperLogLog lab, tune Precision (b bits) to change bucket count and expected error σ, scrub Stream size to grow cardinality, and watch the register bar chart fill. Compare the HLL estimate to the exact distinct count, then use Small stream, High cardinality, or Low precision presets before predicting whether the estimate lands within expected σ.

Simple Fundamental Explanation

Imagine you are standing outside a massive stadium, and you want to estimate how many unique people walked inside.

You could give everyone a notebook and ask them to write down their name, but the notebook would become impossibly heavy.

Instead, you use a trick based on probability: You ask everyone to flip a coin until they get “Heads” and tell you the number of flips it took.

  • If someone says “1 flip”, that’s common (50% chance). You assume there are at least 2 people.
  • If someone says “5 flips”, that’s rare ( chance). You assume there are at least 32 people.
  • If someone says “20 flips”, that’s incredibly rare (1 in a million). You assume there must be around a million people inside!

By recording only the maximum number of consecutive coin flips anyone reported, you can guess the total number of unique people. You don’t need to remember anyone’s name, just the highest run of “Tails” before a “Heads”.

This is the intuition behind HyperLogLog (HLL). It counts the number of distinct elements (cardinality) in a massive dataset using virtually zero memory.


Deep Dive: How It Works Under the Hood

HyperLogLog applies this coin-flipping logic using hash functions.

When you hash data, the output looks like a random string of 0s and 1s. A 0 is “Tails”, a 1 is “Heads”. The hash 0001011... has three leading zeros. The probability of a hash starting with zeros is .

The Basic Algorithm (Flajolet-Martin)

  1. Hash every item in the dataset.
  2. Count the number of leading zeros in the binary representation of each hash.
  3. Keep track of the maximum number of leading zeros seen so far ().
  4. The estimated cardinality is .

The Problem: High Variance

The basic algorithm is wildly inaccurate. A single lucky hash with 20 leading zeros early on will permanently skew the estimate.

The Solution: Stochastic Averaging (The “Hyper” in HLL)

To fix the variance, HyperLogLog divides the data into multiple “buckets” (registers).

  1. The first few bits of the hash are used to route the item to a specific bucket.
  2. The remaining bits are used to count the leading zeros.
  3. Each bucket keeps track of its own .
  4. Finally, you take the harmonic mean of the estimates from all buckets to get a highly accurate global estimate.

Visual Diagram

Input Stream: ["userA", "userB", "userC", ...]

1. Hash the inputs into 32-bit integers:
"userA" -> 01 | 00010110... (Bucket 1, 3 leading zeros)
"userB" -> 11 | 01001011... (Bucket 3, 1 leading zero)
"userC" -> 01 | 00001000... (Bucket 1, 4 leading zeros)

2. Route to Buckets (using first 2 bits -> 4 buckets):
Registers: [ Bucket 0 | Bucket 1 | Bucket 2 | Bucket 3 ]

3. Update Maximum Leading Zeros per Bucket:
Registers: [    0     |    4     |    0     |    1     ]
                         ^ (Updated by userC)

4. Calculate Harmonic Mean of all buckets -> Output Estimate

Practical Example: Counting Daily Active Users

Redis, a popular in-memory database, uses HyperLogLog to count unique visitors to a website.

If a website gets 100 million unique visitors a day, storing every user ID (e.g., 64-bit integers) in a standard hash set would require roughly 800 Megabytes of RAM per day.

Using Redis’s PFADD and PFCOUNT commands (which implement HyperLogLog), you add user IDs to the structure. The standard Redis HLL uses 16,384 buckets. No matter if you insert 10 thousand or 100 million users, the memory footprint remains fixed at 12 Kilobytes. The trade-off is an error rate of ~0.81%.


The Mathematics: Equations and In-Depth Analysis

1. Estimating Cardinality

Let be the number of buckets, and be the maximum number of leading zeros recorded in bucket . The raw estimate using the harmonic mean is:

Where:

  • is a constant correction factor used to correct systematic multiplicative bias. It depends on the number of buckets . For large (e.g., ), .
  • The summation term is the harmonic mean of , preventing large outliers from skewing the result.

2. Space Complexity

If we expect up to unique items, the hash needs to be at least bits long. The maximum number of leading zeros we can observe is . To store this maximum value, each bucket needs to be large enough to hold the number , which requires bits.

Thus, to store buckets, the total memory required is:

This factor is where “HyperLogLog” gets its name, and why it requires so little memory.

3. Error Rate

The standard error of the HyperLogLog estimate is strictly bounded by:

If you want a ~1% error rate, you set , which means you need buckets. At 6 bits per bucket, that’s just ~8 KB of memory.

Lab

HyperLogLog tracks the longest run of leading zeros per bucket. Harmonic mean of registers estimates cardinality — same SHA-256 recipe as hyperloglog.py.

8 bits, 256 buckets
500 events, 350 distinct keys
bucket 0: 3bucket 1: 4bucket 2: 0bucket 3: 0bucket 4: 8bucket 5: 9bucket 6: 3bucket 7: 1bucket 8: 2bucket 9: 0bucket 10: 1bucket 11: 0bucket 12: 6bucket 13: 0bucket 14: 0bucket 15: 2bucket 16: 0bucket 17: 0bucket 18: 3bucket 19: 5bucket 20: 1bucket 21: 2bucket 22: 4bucket 23: 1bucket 24: 3bucket 25: 3bucket 26: 3bucket 27: 2bucket 28: 3bucket 29: 1bucket 30: 0bucket 31: 0bucket 32: 3bucket 33: 0bucket 34: 1bucket 35: 6bucket 36: 1bucket 37: 1bucket 38: 1bucket 39: 2bucket 40: 0bucket 41: 4bucket 42: 2bucket 43: 2bucket 44: 3bucket 45: 1bucket 46: 0bucket 47: 5bucket 48: 4bucket 49: 7bucket 50: 6bucket 51: 4bucket 52: 0bucket 53: 0bucket 54: 2bucket 55: 2bucket 56: 0bucket 57: 3bucket 58: 4bucket 59: 3bucket 60: 6bucket 61: 2bucket 62: 2bucket 63: 2

Distinct keys in stream: user_221, user_110, user_9, user_13, user_1

Will the HLL estimate be within expected σ (±6.5%) of the exact distinct count (350) for this stream?

256 registers, 350 unique keys. Estimate 359 (error 2.6%).

Run implementations

Edit and run Python in the browser (Pyodide), browse the Rust reference in CodeMirror, or open the full sources on GitHub.

Python in the browser (stdlib only).

hyperloglog.py on GitHub

Source

Click Run Python to download the in-browser runtime (first run may take 30–60s).

Output