By the end of this lesson
  • Deconstruct the Flajolet-Martin algorithm and HyperLogLog harmonic mean registers for cardinality estimation
  • Derive the standard error formula sigma = 1.04 / sqrt(m) and calculate memory requirements for arbitrary precision bounds
  • Construct the Count-Min Sketch (d x w) 2D counter matrix and prove the non-underestimation error invariant
  • Implement an algorithmic TypeScript simulation of 64-bit HyperLogLog and Count-Min Sketch frequency counters

1. The Problem & Why It Matters

In modern web applications, distributed systems, and real-time streaming engines, two questions arise constantly:

  1. Cardinality Estimation (Count-Distinct): “How many unique daily active users (DAUs) visited our site today?”
  2. Frequency Estimation (Heavy Hitters): “Which IP addresses are making the most API requests right now to detect DDoS attacks?”

The naive approach stores every seen identifier in a Hash Set or Hash Map:

  • Tracking 100 million unique 64-bit user IDs in a hash set requires:
  • If you need to compute distinct users across 1,000 dimensions (per country, per device type, per campaign), memory balloons into terabytes of RAM.
  • Distributed sets cannot be unioned without massive network bandwidth transfers.

When an approximate answer with a small, mathematically bounded error margin (e.g. ) is acceptable, Probabilistic Data Structures provide near-miraculous efficiency:

  • HyperLogLog (HLL) estimates billions of unique elements with 0.81% standard error using only 12 KB of memory.
  • Count-Min Sketch (CMS) counts frequencies of billions of streaming events in fixed sub-megabyte memory with strict mathematical error bounds.

2. Intuitive Mental Model

The HyperLogLog Coin-Flip Intuition

Imagine a person flips a fair coin repeatedly and tells you: “My longest run of consecutive heads was 10 heads in a row.”

  • Probability of seeing 10 heads in a single attempt:
  • You can deduce with high probability that this person flipped coins roughly times. If someone tells you their longest run was 30 heads, they must have flipped coins roughly times!
  • HyperLogLog applies this principle to uniform 64-bit hashes: the number of leading zeros in the hash represents consecutive coin flips.

The Count-Min Sketch Room Allocation Intuition

Imagine counting attendees wearing different colored shirts in a stadium with 5 doors. Each attendee picks 3 random rooms based on their name. If a room has 15 people in it, you know there cannot possibly be more than 15 people wearing that specific shirt. By taking the minimum count across all 3 rooms, you isolate the true count from random crowd overlaps.

SYSTEM ARCHITECTURE BLUEPRINTDATA STRUCTURES
Probabilistic Data Structures: HyperLogLog and Count-Min Sketch
ARCHITECTURAL FLOW · STEP 1 OF 3

1. hash64(element) → 64-bit Binary String & m = 2^p Registers (M_0 to M_m-1)

Establishes the ingress and structural boundary across hash64(element) → 64-bit Binary String & m = 2^p Registers (M_0 to M_m-1): [0110] [0000 0001 1011 0100 ... 1001]; p=4 bits: Register Index (Bucket 6) | 60 bits: Leading Zeros Count ρ = 7.

Architectural Invariant: p=4 bits: Register Index (Bucket 6) | 60 bits: Leading Zeros Count ρ = 7.
ARCHITECTURAL FLOW · STEP 2 OF 3

2. Harmonic Mean Raw Estimate Formula & Mathematical Error Guarantees

Coordinates core data-plane execution and state transitions through Harmonic Mean Raw Estimate Formula & Mathematical Error Guarantees: E = &#945;_m * m^2 * [ &#931; (2^(-M[j])) ]^(-1); Standard error &#963; &#8776; 1.04 / &#8730;m; For m = 16,384 registers (p=14): Memory = 12 KB in Redis! Estimates billions of unique items with < 0.81% error.

Architectural Invariant: Guarantees: true_count &#8804; estimate &#8804; true_count + &#949; * N.
ARCHITECTURAL FLOW · STEP 3 OF 3

3. 18 & h3(x)

Enforces durability, state convergence, and output verification across 18 & h3(x): Count(x) = min(14, 18, 11) = 11.

Architectural Invariant: Count(x) = min(14, 18, 11) = 11.

3. Architectural Mechanisms & Concrete Topologies

3.1 HyperLogLog (HLL) Architecture

HyperLogLog partitions elements into registers:

  1. Hash input element with a uniform 64-bit hash function: .
  2. Use the first bits as the register index: .
  3. Count the number of leading zeros in the remaining bits, plus 1: .
  4. Update register :

3.2 Count-Min Sketch (CMS) Architecture

A 2D array of counters of depth (rows) and width (columns), with independent pairwise hash functions :

  • Update(): For each row :
  • Query(): Return the minimum value:

4. Formal Mathematical Derivations

4.1 HyperLogLog Harmonic Mean & Standard Error

The raw cardinality estimator uses the harmonic mean of across all registers:

where is a bias-correction constant: For :

Theoretical Standard Error:

For ( registers, as used in Redis): With a 64-bit hash and prefix bits, the remaining suffix has bits, so (plus 0 for an empty register states), requiring per register: (Redis prepends a 16-byte HYLL header, bringing a dense Redis HyperLogLog object to exactly .)

4.2 Count-Min Sketch Error Bounds

Assume a non-negative stream (cash-register / strict turnstile model where every update satisfies , and total stream mass is ) and pairwise-independent hash functions. Given additive error factor and failure probability , we dimension the matrix:

The Guarantees (derived via Markov’s inequality on per-row expected collision noise , so , and across independent rows ):

  1. One-Sided Lower Bound: (never underestimates when all updates ).
  2. Probabilistic Upper Bound: With probability at least :

Worked Sizing Example: For (additive error of total stream events ) and ( confidence):

  • columns; rows.
  • Using 32-bit (4-byte) unsigned counters: —fixed memory whether is or events.

5. Edge Cases, Failure Modes & Distributed Anomalies

5.1 Small Cardinality Bias in HLL

When , many registers remain zero. The raw harmonic mean formula overestimates cardinality. The Fix (Linear Counting Fallback): Count the number of empty registers . If :

5.2 Mergeability: The Ultimate Distributed Superpower

Both HyperLogLog and Count-Min Sketch are monoids under point-wise maximum/addition!

  • To compute unique users across 10 distributed Cassandra nodes: each node computes its local 12 KB HLL sketch. The coordinator fetches ten 12 KB buffers ( network transfer) and merges them: The merged sketch has the exact same accuracy as if all data had been processed on a single machine!

6. Open-Source Architectural Studies

6.1 Redis: PFADD, PFCOUNT, PFMERGE

Redis implements HyperLogLog natively. An HLL structure begins in sparse representation (run-length encoded bytes for low cardinality). When memory exceeds 3 KB, Redis converts it to the dense representation of 16,384 6-bit registers, capped permanently at 12 KB.

6.2 Apache Pinot & ClickHouse: Approximate Aggregations

ClickHouse uses uniqCombined64 (a hybrid HyperLogLog variant) to compute SELECT count(DISTINCT user_id) over billions of rows in milliseconds, scanning memory orders of magnitude faster than exact SQL hash sets.


7. Production Incident Post-Mortem (War Stories)

The Billion-Key Redis OOM Blackout

Context: An ad-tracking service counted daily unique cookie clicks per advertiser campaign using Redis SADD campaign:123:cookies <cookie_id>. The Incident: During a global marketing event, campaigns tracked 45 million unique cookies each. Redis memory surged from 14 GB to 128 GB within 3 hours, triggering the Linux OOM killer (kill -9 redis-server). The Remediation: Replaced SADD / SCARD with Redis HyperLogLog PFADD / PFCOUNT. Memory dropped from 128 GB to 85 MB (a memory reduction), while distinct reporting error remained below 0.8%.


8. Production-Grade TypeScript Simulation: HyperLogLog & Count-Min Sketch

/**
 * Production Probabilistic Simulation: HyperLogLog & Count-Min Sketch
 * Browser- & Server-portable (uses standard Uint8Array / TextEncoder, zero Node globals).
 */

export class HyperLogLogSimulator {
  private readonly p: number;
  private readonly m: number;
  private readonly registers: Uint8Array;
  private readonly alphaMM: number;
  private readonly encoder = new TextEncoder();

  constructor(p = 12) {
    this.p = p;
    this.m = 1 << p;
    this.registers = new Uint8Array(this.m);

    const alpha = 0.7213 / (1 + 1.079 / this.m);
    this.alphaMM = alpha * this.m * this.m;
  }

  private hash64(key: string): bigint {
    // 64-bit FNV-1a followed by MurmurHash3 fmix64 avalanche finalizer
    let hash = 0xcbf29ce484222325n;
    const fnvPrime = 0x100000001b3n;
    const mask64 = 0xffffffffffffffffn;
    const bytes = this.encoder.encode(key);

    for (let i = 0; i < bytes.length; i++) {
      hash ^= BigInt(bytes[i]);
      hash = (hash * fnvPrime) & mask64;
    }

    // fmix64 finalizer ensures uniform entropy across upper register-index bits
    hash ^= hash >> 33n;
    hash = (hash * 0xff51afd7ed558ccdn) & mask64;
    hash ^= hash >> 33n;
    hash = (hash * 0xc4ceb9fe1a85ec53n) & mask64;
    hash ^= hash >> 33n;
    return hash;
  }

  public add(element: string): void {
    const hash = this.hash64(element);
    const registerIndex = Number(hash >> BigInt(64 - this.p));
    const remainingBits = (hash << BigInt(this.p)) & 0xffffffffffffffffn;
    const maxRank = 64 - this.p + 1;

    // rho(w): 1-based position of first 1-bit in remaining (64 - p) suffix bits
    let rank = 1;
    let mask = 0x8000000000000000n;
    while ((remainingBits & mask) === 0n && rank < maxRank) {
      rank++;
      mask >>= 1n;
    }

    if (rank > this.registers[registerIndex]) {
      this.registers[registerIndex] = rank;
    }
  }

  public count(): number {
    let sum = 0;
    let emptyRegisters = 0;

    for (let i = 0; i < this.m; i++) {
      const val = this.registers[i];
      sum += Math.pow(2, -val);
      if (val === 0) emptyRegisters++;
    }

    let estimate = this.alphaMM / sum;

    // Small cardinality bias correction (Linear Counting)
    if (estimate <= 2.5 * this.m && emptyRegisters > 0) {
      estimate = this.m * Math.log(this.m / emptyRegisters);
    }
    return Math.round(estimate);
  }
}

export class CountMinSketchSimulator {
  private readonly width: number;
  private readonly depth: number;
  private readonly table: Int32Array[];

  constructor(width = 500, depth = 5) {
    this.width = width;
    this.depth = depth;
    this.table = Array.from({ length: depth }, () => new Int32Array(width));
  }

  private hash(key: string, seed: number): number {
    let h = seed >>> 0;
    for (let i = 0; i < key.length; i++) {
      h = Math.imul(h ^ key.charCodeAt(i), 0x5bd1e995) >>> 0;
    }
    h ^= h >>> 13;
    h = Math.imul(h, 0x5bd1e995) >>> 0;
    h ^= h >>> 15;
    return (h >>> 0) % this.width;
  }

  public add(key: string, count = 1): void {
    for (let row = 0; row < this.depth; row++) {
      const col = this.hash(key, row * 0x9e3779b1 + 17);
      this.table[row][col] += count;
    }
  }

  public addConservative(key: string, count = 1): void {
    const targetValue = this.estimate(key) + count;
    for (let row = 0; row < this.depth; row++) {
      const col = this.hash(key, row * 0x9e3779b1 + 17);
      if (this.table[row][col] < targetValue) {
        this.table[row][col] = targetValue;
      }
    }
  }

  public estimate(key: string): number {
    let minCount = Infinity;
    for (let row = 0; row < this.depth; row++) {
      const col = this.hash(key, row * 0x9e3779b1 + 17);
      minCount = Math.min(minCount, this.table[row][col]);
    }
    return minCount;
  }
}

export function runProbabilisticExperiment(): void {
  console.log("=== 1. HyperLogLog Cardinality Verification ===");
  const hll = new HyperLogLogSimulator(12); // m=4096, theoretical sigma = 1.04 / 64 = 1.625%
  const TRUE_DISTINCT = 20000;

  for (let i = 0; i < TRUE_DISTINCT; i++) {
    hll.add(`user_${i}`);
  }

  const estimated = hll.count();
  const errorPercent = (Math.abs(estimated - TRUE_DISTINCT) / TRUE_DISTINCT) * 100;
  console.log(`True Distinct: ${TRUE_DISTINCT} | HLL Estimated: ${estimated} | Error: ${errorPercent.toFixed(2)}%`);

  console.log("=== 2. Count-Min Sketch (Standard vs. Conservative Update) ===");
  const standardCms = new CountMinSketchSimulator(256, 5);
  const conservativeCms = new CountMinSketchSimulator(256, 5);

  standardCms.add("ip_192.168.1.1", 450);
  conservativeCms.addConservative("ip_192.168.1.1", 450);

  for (let i = 0; i < 5000; i++) {
    const noiseKey = `ip_noise_${i % 400}`;
    standardCms.add(noiseKey, 1);
    conservativeCms.addConservative(noiseKey, 1);
  }

  console.log(
    `Heavy Hitter True: 450 | Standard CMS: ${standardCms.estimate("ip_192.168.1.1")} | Conservative CMS: ${conservativeCms.estimate("ip_192.168.1.1")}`
  );
}

runProbabilisticExperiment();

9. Operational Checklist, Failure Runbooks & Sizing Rules

When to Use Probabilistic Data Structures

  • Use HyperLogLog: When counting unique entities (DAU/MAU, IP addresses, search queries) where accuracy is acceptable and dataset exceeds – items.
  • Use Count-Min Sketch: For point frequency estimation, DDoS heavy-hitter tracking (paired with a bounded Top-K Min-Heap), and frequency-aware cache admission (W-TinyLFU).
  • Never Use Probabilistic Structures: For financial accounting balances, exact billing ledgers, inventory deductions, or security revocation lists where exactness is mandatory.

10. Design Alternatives & Trade-offs Matrix

StructureQuery AnsweredMemory ComplexityError ModeDistributed Merge?Best Production Fit
Exact Hash Set / MapMembership, exact distinct, exact frequency linear in unique keys (exact)Expensive ( payload transfer)Billing ledgers, small cardinalities ()
Bloom FilterSet membership (mightContain) bits ( bits/key at FPR)False positives ; zero false negativesYes (bitwise OR across identical shapes)SSTable negative-lookup gating, cache miss shielding
HyperLogLog (HLL)Cardinality (COUNT(DISTINCT)) ( for )Two-sided unbiased ()Yes (register-wise max in )DAU/MAU analytics, multi-dimensional OLAP rollups
Count-Min Sketch (CMS)Point frequency (count(x)) countersOne-sided overestimate ( w.p. )Yes (cell-wise + for standard CMS)Streaming rate limiting, W-TinyLFU cache admission
Space-Saving / Misra-GriesTop- heavy hitters directly deterministic countersDeterministic additive bound Approximate mergeDirect Top- tracking when key labels must be retained

11. Guided Applied System Design Exercises & Debugging Drills

Drill 1: Distributed HLL Union across Edge PoPs

Your CDN operates 50 edge Points of Presence (PoPs) globally. Each PoP logs 100 million HTTP requests daily.

  1. Prove why transmitting raw user IP sets to central servers requires gigabytes of bandwidth, whereas transmitting local 12 KiB HLL sketches requires only total.
  2. Write the register-wise max merge algorithm to compute the global unique user count from the 50 regional sketches, and state what breaks if two PoPs used different hash seeds or precision parameters .

Drill 2: Top-K Heavy Hitters with CMS & Min-Heap

Design a real-time DDoS mitigation engine that identifies the top 100 IP addresses generating traffic over a 10 Gbps network link.

  1. Combine a Count-Min Sketch (which stores no keys) with an in-memory Min-Heap of size keyed by estimated frequency.
  2. Outline how you prevent stale historical spikes from dominating continuous long-lived windows using tumbling dual-sketch rotation or exponential counter halving.
View Reference Architecture & Worked Solutions (CORE-M08-005)

1. Worked Solution — Drill 1: Distributed HLL Union Across 50 Edge PoPs

  1. Bandwidth Proof (Raw Set Shipping vs. HLL Sketch Shipping):

    • Because the same roaming user or bot IP can hit multiple PoPs, PoPs cannot simply send scalar integers count_i to be summed centrally ( due to cross-PoP duplicates).
    • Naive Exact Union: Assuming each of the 50 PoPs observes unique IPv6 (16-byte) or hashed (8-byte) identifiers out of requests, shipping raw deduplicated sets to the central aggregator transfers:
    • HyperLogLog Sketch Shipping ( registers ): This achieves a reduction in WAN payload while preserving standard error on the global union .
  2. Register-Wise max Merge Algorithm & Compatibility Invariants:

    function mergeHLLSketches(sketches: Uint8Array[], m = 16384): Uint8Array {
      const merged = new Uint8Array(m);
      for (const sketch of sketches) {
        if (sketch.length !== m) {
          throw new Error(`Incompatible HLL precision: expected m=${m}, got ${sketch.length}`);
        }
        for (let j = 0; j < m; j++) {
          if (sketch[j] > merged[j]) {
            merged[j] = sketch[j];
          }
        }
      }
      return merged;
    }
    • Why Identical Hash Seeds & Precision Are Mandatory:
      • Different Hash Seeds: If uses seed and uses seed , the same user hashes to two independent registers with two independent ranks . Merging them treats as two distinct users, inflating the union toward instead of .
      • Different Precision : If () and (), register indices do not align 1-to-1. To merge them, the higher-precision sketch () must first be folded (downsampled) to before merging.

2. Worked Solution — Drill 2: Top-K Heavy Hitters with CMS & Min-Heap

  1. Combined CMS + Bounded Min-Heap () Architecture:

    • A Count-Min Sketch is a counter matrix (d x w) that does not store original keys, so you cannot enumerate the top 100 IPs from the CMS alone.
    • Pair the CMS (e.g., , using Conservative Update) with an auxiliary MinHeap<IpEntry> of maximum capacity and a companion Map<string, HeapNode> for membership lookup:
      1. On packet arrival for ip, update the CMS: cms.addConservative(ip, 1) and read est = cms.estimate(ip).
      2. If ip is already in the MinHeap, update its key to est and siftDown in .
      3. Else if heap.size < K, insert (ip, est) into the MinHeap in .
      4. Else if est > heap.peekMin().est, evict heap.popMin() and insert (ip, est) in .
    • Memory Footprint: for the CMS plus for the Top-100 Min-Heap ( total RAM to monitor a link at line rate!).
  2. Preventing Stale Historical Spikes (Windowed Aging):

    • Because standard and conservative CMS counters only increase monotonically, a bursty IP from 6 hours ago would permanently occupy the Top-100 heap unless aged out. Two production aging strategies solve this:
      • Strategy A — Tumbling Dual-Sketch Rotation (Active / Warmup): Maintain two CMS + Heap pairs covering windows staggered by . Every , zero out the older sketch (table.fill(0)) so queries always read from a sketch warmed over the last .
      • Strategy B — Exponential Counter Decay (Halving, as in W-TinyLFU): Every events (e.g., packets), right-shift every counter in the CMS and Min-Heap by 1 bit (counter >>>= 1), exponentially decaying historical bursts with a predictable half-life while preserving recent heavy hitters.

12. Authoritative References & Version Context

MASTERY GATE · STEP 20 OF 94

Knowledge check & lesson completion

Score 4 / 4 (100%) on this check to mark this lesson Completed ✓, advance your position on the Master Spine, and add these concepts to your Spaced-Repetition Review Deck.

1. How does HyperLogLog estimate the cardinality of a billion unique elements using only ~12 KiB of dense register memory in Redis?
2. Under a strict non-negative stream (increments c >= 0), why is a Count-Min Sketch frequency query guaranteed NEVER to underestimate the true count of an item?
3. How does the 'Conservative Update' optimization reduce overestimation error in an insert-only Count-Min Sketch, and what trade-off does it introduce?
4. Why does HyperLogLog aggregate its m register estimates 2^{M[j]} using the Harmonic Mean rather than the Arithmetic Mean?
PERSONAL NOTEBOOK

Chapter Notes

Personal engineering insights and observations saved to this browser.

0 notes
Tip: You can also highlight text in the article above to quote directly.

Loading personal notes...