---
topic: system-design
author: Crashtech Editorial
date: Aug 16, 2026 · read: 6 min
---

Count Billions in 12 Kilobytes

HyperLogLog estimates cardinality by reading leading zeros in hashed values, trading 1% error for fixed memory.

A video streaming site with 500 million viewers a day needs to know: how many unique visitors did we reach this month? The obvious approach stores every visitor ID in a set. A month of data = 2.5 billion unique IDs × 8 bytes each = 20 gigabytes. One line of a company dashboard now occupies a server’s entire memory budget.

Naive approach stores every visitor ID in gigabytes of RAM. HyperLogLog estimates cardinality by hashing IDs and reading leading-zero distribution in fixed 12 KB.

The breakthrough: trade 1% accuracy for a fixed 12 KB footprint, regardless of cardinality.

The Naive Approach: Why Exact Counting Dies

Storing every unique visitor ID works perfectly until scale hits:

def count_unique(visitor_ids):
    return len(set(visitor_ids))  # O(1) lookup, O(N) space

For 1 billion visitors over a month:

  • 8 bytes per ID (typical UUID or long integer)
  • 1 billion × 8 = 8 gigabytes of RAM
  • Growth is linear with cardinality; no escape

This approach breaks in three ways: memory exhaustion, distributed merge failure (combining counts across data centers means re-storing all IDs), and query latency (set operations are CPU-friendly but memory I/O eventually saturates). Web-scale companies hit this wall within weeks of launch.

HyperLogLog: The Probabilistic Breakthrough

HyperLogLog (Flajolet et al., 2007) inverts the problem. Instead of storing IDs, it stores only the distribution of a statistical property: the leading-zero count in hashed values.

The key insight: if a hash function produces a random bit string, observing N leading zeros is extraordinarily rare (probability 1 in 2^N). Seeing many distinct items with leading zeros implies you’ve hashed billions of times.

Think of it like sampling: if you flip a coin and get heads 10 times in a row, you probably flipped it far more than 10 times total. Similarly, a hash with 20 leading zeros implies the algorithm has seen roughly 2^20 distinct items.

This trade-off is brutal in its efficiency:

Exact counting (set) O(N) space

Perfect accuracy, linear memory growth, merge requires reshashing everything.

HyperLogLog O(log log N) space

~1% error, fixed 12 KB, mergeable registers, scales from millions to billions.

How It Works: The Mechanism

HyperLogLog operates in four steps:

  1. Hash every value

    Apply a cryptographic hash (SHA-1, MD5) to each incoming item. For a visitor ID like user_7429814, compute its binary hash.

  2. Extract leading-zero count

    Count the number of leading zero bits in the hash output. For example:

    • Hash 0x7c2a... in binary = 0111110... → 2 leading zeros
    • Hash 0xc8f9... in binary = 1100... → 0 leading zeros
    • Hash 0x002e... in binary = 00000000... → 8 leading zeros

    Leading zeros are rare: roughly 1 in 2^k values have k leading zeros.

  3. Update register buckets

    Use the first 14 bits of the hash to select one of 16,384 registers (14 KB total). Update that register with the maximum leading-zero count seen so far for its bucket. This step is critical: tracking the maximum across billions of hashes requires only a single byte per register.

  4. Calculate the harmonic mean

    The cardinality estimate is:

    E = α × (2^14)^2 / (sum of 2^(-register_i))

    where α ≈ 0.7213 (a calibration constant) and register_i is the stored leading-zero count for bucket i. The harmonic mean formula captures the probabilistic structure of the algorithm—it yields an unbiased estimate with about ±1% error.

HyperLogLog mechanism: hash produces binary strings; the leading-zero run is rare (like flipping heads 10 times means you flipped many times); register buckets track the maximum leading-zero count per hash prefix.

Step by step: how the leading-zeros trick becomes a cardinality counter.

Why the harmonic mean?

The sum Σ(2^(-M_i)) where M_i is the leading-zero count for register i, directly encodes the probabilistic behavior. Registers with small M values (few leading zeros, common) contribute 2^0 = 1. Registers with large M values (many leading zeros, rare) contribute tiny amounts like 2^(-8) = 1/256. The harmonic mean (inversion of the arithmetic mean) extracts the right weighting to produce an unbiased cardinality estimate. Simple averaging would systematically underestimate at high cardinalities.

Memory Efficiency: The Numbers

The fixed footprint is the algorithm’s superpower:

  • 16,384 registers × 1 byte per register = 16 KB (for the counter)
  • Auxiliary data (calibration constants, merge buffers) ≈ 1–2 KB
  • Total ≈ 12–16 KB per HyperLogLog instance

Compare to exact cardinality:

  • 1 million unique items: 8 MB (set) vs. 12 KB (HyperLogLog) = ~650× smaller
  • 1 billion unique items: 8 GB (set) vs. 12 KB (HyperLogLog) = ~650,000× smaller

Memory doesn’t grow. Ever. You can count the entire internet’s daily unique visitors and pay only 12 KB, accepting 1% error.

Merging and Distributed Counting

One counter becomes many. Imagine you run analytics across 100 regional servers, each tracking visitor counts locally:

# Each server maintains its own HyperLogLog
server_1_hll = HyperLogLog()
server_2_hll = HyperLogLog()
server_100_hll = HyperLogLog()

# Merge: take element-wise maximum of registers
global_hll = merge(server_1_hll, ..., server_100_hll)
# Merge cost: O(1) per bucket

Merging two HyperLogLog instances is trivial: compare each of the 16,384 register pairs and keep the maximum. The resulting HyperLogLog estimates the union cardinality with the same ~1% error. This is impossible with exact-count sets (you’d have to rehash all items across all servers).

This design is why Redis and streaming platforms use HyperLogLog for global unique-user tracking—it scales horizontally with negligible overhead.

Advertisement

Trade-Offs: When NOT to Use HyperLogLog

HyperLogLog excels at scale but breaks down in three scenarios:

Do

  • Cardinality in the millions or higher (1% error is noise at that scale)
  • Distributed systems with many sources to merge
  • Memory-constrained environments (embedded, edge)
  • Real-time dashboards where exact counts aren’t critical (YouTube video views, Twitter impressions)

Don't

Do

Don't

  • Exact cardinality required (e.g., legal/billing counts, audit logs)
  • Small datasets where a set fits in memory (< 10 million items, memory is cheap)
  • You need to output the actual unique items (HyperLogLog is count-only)
  • Adversarial input (if someone can craft hashes that collide on leading zeros, they can skew estimates)

Real-World Deployments

Redis: The PFADD and PFCOUNT commands implement HyperLogLog. Twitter uses it for tracking daily active users; Stripe uses it for fraud-ring detection (unique card fingerprints across millions of transactions).

Google Analytics: Uniqueness estimates in real-time dashboards rely on HyperLogLog’s speed and memory efficiency. For 2 billion daily sessions, storing exact counts would require petabyte-scale infrastructure.

Bloom Filters + HyperLogLog: Combine the two for approximate set membership + cardinality. A Bloom filter answers “is this item in the set?” (with false positives); HyperLogLog answers “how many unique items have we seen?” Together they enable space-efficient streaming deduplication.

The Deeper Math (For the Curious)

The algorithm’s error bound comes from the standard error of the harmonic-mean estimator:

Standard Error ≈ 1.04 / sqrt(m)

where m is the number of registers (16,384 in standard HyperLogLog). This gives a typical error of:

1.04 / sqrt(16384) ≈ 0.0081 ≈ 0.81%

By adding a bias-correction term for small and large cardinalities, practical implementations achieve ±1% error across the full range (1 to 2^64 unique items). The trade-off is proven: no way to get this compression without accepting probabilistic error.

Takeaway

HyperLogLog solves cardinality estimation by treating it as a statistical inference problem rather than an enumeration problem. Instead of storing data, it stores a fingerprint of the leading-zero distribution—a distribution that encodes cardinality probabilistically. The result: a fixed 12 KB counter that scales from millions to billions of unique items with 1% error and mergeable distributed instances.

For any system tracking uniqueness at scale, HyperLogLog is the answer to “how do you avoid gigabytes of storage?”

Advertisement

Frequently asked questions

How does HyperLogLog count unique items without storing them?

It hashes every item and examines the distribution of leading zeros in the binary hash output. Since leading zeros are rare (like flipping heads repeatedly), observing many leading zeros statistically implies you've hashed billions of distinct items.

What is the memory cost of HyperLogLog?

Fixed at 12 KB regardless of cardinality. It maintains 16,384 registers (one per possible 14-bit hash prefix), each storing the maximum leading-zero count observed for that prefix. This tiny footprint works from millions to billions of unique items.

Why use the harmonic mean instead of a simple average?

The harmonic mean naturally handles the 2^(-M) probabilities that underpin the leading-zeros calculation, yielding an unbiased cardinality estimate. Simple averaging would produce systematic errors at very high cardinalities.

When should I use HyperLogLog instead of exact counting?

When cardinality is in the millions or higher and 1% error is tolerable. Real-world use cases: unique visitor tracking, deduplication streams, set intersection size estimation. Skip it for small sets where exact counts fit in memory.

Does HyperLogLog work well with merging data from multiple servers?

Yes—one of its biggest advantages. Merge two HyperLogLog instances by taking the element-wise maximum of their registers. This enables exact-level accuracy while counting globally across a distributed cluster.

Sources & further reading

/* Comments */