System designDeep dive10 min
Count-Min Sketch & top-K
Finding the most played songs, the busiest IP addresses or the hottest cache keys needs a count for every item, and there may be millions of items in every time window. A Count-Min Sketch estimates any item's count in a few kilobytes, never too low and rarely much too high, and small top-K structures keep the leaders without counting everything else.
1 · The problem
A trending chart needs a count for every item in every window
A music service shows a "trending now" chart for each of 200 countries, refreshed every minute. In a busy country, 5 million plays an hour are spread over about a million different tracks. To rank them, the service needs a play count per track, per country, per minute, and the hour's chart is built from the last 60 minutes.
An exact counter per track costs about 16 bytes, an 8-byte track ID and an 8-byte count, before any hash-map overhead:
Exact counts | Memory |
|---|---|
One busy country, one minute: up to 83,000 tracks played (5 million plays an hour ÷ 60) × 16 bytes | 1.3 MB |
× 60 minute windows | 80 MB |
× 200 countries, if all were that busy | 16 GB |
Most of it is wasted. Play counts are extremely skewed: a few hit songs take a large share of plays, and most tracks are played once or twice an hour (Hot partitions & skew). The chart only needs the counts of the few leaders, but nobody knows in advance which tracks those are. Two kinds of structure solve this: a Count-Min Sketch estimates any track's count in fixed memory, and a top-K structure keeps just the leaders.
2 · The structure
Count-Min adds each item to one counter per row and reads the smallest
A Count-Min Sketch is a grid of counters, d rows of w counters each, all starting at zero, with a different hash function per row. Each play of a track adds 1 to one counter in every row, the one its hash picks. To read a track's count, look at the same d counters and take the minimum:
Updating and reading both touch exactly d counters, whatever the number of tracks. The grid's size is fixed when it is created.
Why the minimum? Every counter "Night Drive" touches includes all of its plays, plus the plays of any other tracks that hashed to the same counter. Collisions can only add, so every one of the d counters is at least the true count, and the smallest is the closest. That gives the sketch its defining property: it never underestimates. A track estimated at 3,000 plays was played at most 3,000 times.
It also can't list anything. The sketch answers "how many times was this track played?" for a track you name, but it holds no track IDs, so it can't say which tracks are on top. That needs the top-K structures later on this page.
3 · The error
The error is a share of the whole stream, so big counts are accurate and small ones are noise
The sketch is sized from two numbers: an error ε as a fraction of all updates, and a failure probability δ:
- Width w = ⌈e ÷ ε⌉, where e ≈ 2.718.
- Depth d = ⌈ln(1 ÷ δ)⌉.
- Guarantee: the estimate is never below the true count, and with probability at least 1 − δ it is at most the true count + ε × N, where N is the total of all updates.
For the chart, ε = 0.001 and δ = 1% give w = ⌈2,718.3⌉ = 2,719 and d = ⌈ln 100⌉ = ⌈4.61⌉ = 5: 13,595 counters, about 54 KB at 4 bytes each, whether there are a thousand tracks or a billion. With N = 5 million plays, each estimate is at most 0.001 × 5,000,000 = 5,000 too high, with 99% confidence.
The bound is the same 5,000 for every track, and that is the key to using a Count-Min Sketch well. A simulated hour of 5 million plays over a million tracks, with a skewed popularity curve, shows the effect:
Track's rank | True plays | Estimate | Error |
|---|---|---|---|
1 | 347,085 | 347,827 | +742 (0.2%) |
100 | 3,567 | 4,427 | +860 (24%) |
10,000 | 36 | 648 | +612 (17 times the truth) |
The top track is counted almost exactly; a track played 36 times reads as 648. Every error stayed under the 5,000 bound, the largest was 2,774, and they averaged 787. So treat estimates below a threshold around ε × N as noise. That suits a trending chart perfectly: it only cares about the head of the curve, where the error is a tiny share of the count.
When counts only ever increase, a small change helps further. A conservative update raises only the counters that hold the current minimum, and only as far as the new minimum, instead of adding 1 to all d counters. The estimate still never falls below the truth, but other tracks' plays pile up less:
Average overcount across all one million tracks in the same simulation. The tracks ranked 1, 10 and 100 became exact; the gain is smallest for the rarest tracks.
4 · Top-K
Top-K structures keep the leaders, not the whole crowd
The chart needs the 50 most played tracks, and a sketch can't list them. There are two classic ways to keep a list of leaders:
- Sketch plus heap. On each play, update the Count-Min Sketch, read the track's estimate, and keep a min-heap of the K tracks with the highest estimates. If the new estimate beats the smallest in the heap, it replaces it.
- A fixed set of counters. Keep m counters with track IDs, and nothing else. Space-Saving is the best-known algorithm of this kind.
Space-Saving's rule is short. If the track already has a counter, add 1. If not, and a counter is free, start it at 1. If all m counters are taken, give the newcomer the benefit of the doubt: it takes over the smallest counter and adds 1 to it, recording the old value as its possible overcount. With three counters:
- Plays: A, A, B, C
A: 2, B: 1, C: 1. All three counters are in use.
- Play: D
D isn't tracked. The smallest counter is B's, at 1. D takes it over: D: 2, of which up to 1 may belong to B.
- Play: A
A: 3, still exact. A frequent track keeps its counter and keeps growing.
Two guarantees follow. Every count is at most the smallest counter too high, and the smallest counter is at most N ÷ m. And any track played more times than the smallest counter is certain to be in the list. In the same simulation, 1,000 counters gave:
Space-Saving, m = 1,000 | Result |
|---|---|
Smallest counter | 3,552, under the N ÷ m = 5,000 bound |
Tracks guaranteed to be listed | All 98 with more than 3,552 plays |
Top 10 | Exact counts, in the right order |
Top 100 | The same 100 tracks as the exact ranking |
A thousand counters, a few tens of kilobytes, reproduced the exact top 100 of a million tracks. The skew that makes exact counting wasteful is exactly what makes these structures accurate: the leaders are far ahead, so the small tracks that cycle through the bottom counters never displace them.
5 · Windows
Sketches add up, so windows and servers combine
Plays arrive at dozens of servers, and the chart covers a sliding hour. Count-Min Sketches handle both, because counting is addition: if two sketches use the same width, depth and hash functions, adding them counter by counter gives exactly the sketch of both streams together.
Each server sketches its own plays; an aggregator adds them per minute and keeps the last 60 minute sketches. The chart asks the hour's sketch about candidate tracks from a top-K list.
Redis's CMS.MERGE does the addition, and requires all sketches to have identical width and depth.
Top-K lists combine less neatly, because each list has already forgotten the tracks it dropped. Two common answers: send each server's candidates to the aggregator and look their counts up in the merged sketch, or use a structure that forgets on purpose. HeavyKeeper, behind Redis's Top-K, makes counters of small items decay away probabilistically as other items collide with them, while large items are barely affected, so the leaders stay and the noise fades. For windows in general, see Stream processing.
6 · In practice
Use them for the heavy hitters, where a small absolute error doesn't matter
Question | Structure |
|---|---|
How often did this item occur, roughly? | Count-Min Sketch |
Which items occur most? | Top-K: Space-Saving, HeavyKeeper, or sketch plus heap |
How many distinct items? | HyperLogLog (HyperLogLog: counting distinct things) |
Have we seen this item at all? | Bloom filter (Bloom filters) |
Exact counts for billing or quotas | Exact counters |
Typical uses share one shape: a skewed stream where the big items matter. Trending songs, searches and hashtags; the IP addresses sending the most requests, to spot abuse early (Rate limiting algorithms); the hottest cache or database keys (Hot partitions & skew); network flows. Redis provides both structures, CMS.INITBYPROB to size a sketch from an error and a probability, and TOPK.RESERVE for HeavyKeeper. BigQuery's APPROX_TOP_COUNT returns approximate top elements in a query.
For the music service, the checklist:
- Size from ε and δ: w = ⌈e ÷ ε⌉, d = ⌈ln(1 ÷ δ)⌉, and remember the error is ε × the whole stream.
- Ignore estimates below about ε × N; they are mostly collisions.
- Keep leaders in a top-K structure, and use the sketch to check their counts.
- One sketch per server per minute, same dimensions and hashes, added into windows.
- Use conservative updates where counts only grow.
- Never use estimates where exactness matters, such as royalties paid per play.
Check yourself
5 questions
Takeaways
Remember this
- A Count-Min Sketch is d rows of w counters; each item adds to one counter per row, and its estimate is the smallest of those counters.
- It never underestimates, and with probability 1 − δ overestimates by at most ε × N, with w = ⌈e ÷ ε⌉ and d = ⌈ln(1 ÷ δ)⌉.
- The error is a share of the whole stream, so the sketch is accurate for heavy hitters and useless for rare items.
- To list the leaders, use a top-K structure such as Space-Saving, which finds every item more frequent than its smallest counter.
- Standard sketches merge by adding counters, so per-server and per-minute sketches combine into any window.