Data structures · 11 min
Count-Min Sketch
Count 500 million hashtags a day in about half a megabyte. It may overcount a little, but it never undercounts.
Here is a puzzle. Your app gets 500 million posts a day, each with a hashtag, and about 50 million different hashtags turn up. You need to know how often each one was used, so you can show what is trending.
The catch: the counting box has under 1 MB of memory. The answers may be a little off, but any count you report must be within 50,000 of the truth, at least 99 times in 100. Is that possible?
Why the obvious answer is out
The obvious way is a map from hashtag to count: see #sakura, add 1 to map["#sakura"]. It is exact and easy.
It is also huge. 50 million keys at about 16 bytes of text plus an 8-byte count is 1.2 GB, before the map’s own overhead. Worse, it grows with every new hashtag, and most hashtags are used once or twice and never matter.
So the map is ruled out. Whatever we build has to stay the same size no matter how many different hashtags show up. That means it cannot even remember the hashtags themselves.
The clue: you are allowed to be a little wrong
Look at the rule again: within 50,000. A trending hashtag is used millions of times a day, so being off by 50,000 doesn’t change what trends. A hashtag used 3 times is never going to trend, so you don’t care how wrong its count is.
That freedom is what makes the puzzle solvable.
Attempt one: let hashtags share counters
Take a row of 16 counters, numbered #0 to #15. To count a post tagged #sakura, hash “#sakura”, divide the big number by 16 and keep the remainder. That gives a number from 0 to 15. Add 1 to that counter.
To ask “how many #sakura?”, do the same hash and read the same counter. The row never grows: 16 numbers, whatever tags arrive.
But 50 million hashtags (or even 40) can’t each get their own counter out of 16. Two different tags will often land on the same one. That is called a collision, and it means the counter holds both tags’ posts added together.
Guess first: #sakura was really posted 100 times. #tokyo was posted 40 times and happens to land on the same counter. What do you get when you read #sakura, and can you tell it is wrong?
You read 140, and no, you can’t tell. The counter is just a number; it doesn’t know which tags added to it. All you know for sure is that it is at least 100, because collisions only ever add.
Attempt two: several rows, believe the smallest
The fix is to keep a few rows instead of one, say 4 rows of 16 counters. Each row mixes its own row number into the text before hashing, so #sakura lands on a different counter in each row, and so does every other tag. Its neighbours in row 1 are strangers in row 2.
To add a post, bump one counter in every row. To read a count, look at the same 4 counters and take the smallest. That is the whole structure, and where the name comes from: count in every row, report the minimum.
| Row | Who else landed on #sakura’s counter | Counter reads |
|---|---|---|
| row 1 | #tokyo (40 posts) | 140 |
| row 2 | #fuji (3 posts) | 103 |
| row 3 | nobody | 100 |
| row 4 | #anime (18 posts) | 118 |
Why does the smallest work? Every counter is at least the real count, because collisions only add. So the smallest is the closest. To be badly wrong, #sakura would need to share with a busy tag in every row at once, and each extra row makes that much less likely.
Guess first: You stream 1,000 posts through a 4 × 16 grid, then look up “pizza”, which never appeared. What does the sketch say?
Not 0. 1,000 posts over 16 counters is about 62 per counter, so all 4 of pizza’s counters hold other tags’ posts. The smallest of the four is usually around 20. The sketch can’t tell “never seen” apart from “shares counters with busy tags”. A wider grid helps: at 64 counters per row, pizza usually reads 0.
Try it: a 4 × 16 sketch
- Type #sakura and press Add one. Step 3 shows 4 counters, one per row, each going up by 1. Nothing else in the grid changes.
- Press “+1,000 from the stream”. In the Top 5 table, #sakura is near 270 and its “Over by” is usually 0: popular tags come out almost exact. (The numbered walkthrough goes back to its ramen example.)
- Type pizza and press Look up. Real count 0, but the sketch says something like 20.
- Type unagi (the rarest tag) and press Look up: real about 5, sketch about 11. Switch Rows to 1 and it jumps to around 50. Switch Width to 64 and it shrinks again.
How wrong can it be? Two knobs
The demo just showed the pattern. The overcount on a counter is whatever other tags dumped there, so it depends on how busy the whole stream is, not on the tag you asked about.
Width (counters per row) sets how big the error can get. A good hash spreads posts evenly, so a counter holds about total ÷ width posts on average. In the demo, 1,000 posts over 16 counters is about 62 per counter.
How often is a row badly off? Pick a line at 2.72 times the average: 2.72 × 62 ≈ 170. The 16 counters hold 1,000 posts between them, so at most 5 can hold more than 170 (6 × 170 is already 1,020). Your tag lands on a random counter, so the chance that it gets more than 170 extra is at most 5 in 16.
The general rule: in any one row, the chance of more than 2.72 × total ÷ width extra is at most 1 in 2.72, which is 0.368, about 37%. (Here it came out a little lower, 5 in 16, because you can’t have a fraction of a counter. Any multiple above 1 would work; 2.72, a constant mathematicians call e, happens to need the fewest counters in total.)
Rows set how often you get unlucky. Each row mixes in its own number, so being unlucky in row 1 says nothing about row 2: they behave like separate coin flips. The estimate is only that wrong if every row is unlucky, so with 4 rows the chance is at most 0.368 × 0.368 × 0.368 × 0.368 ≈ 0.018, under 2%.
The demo’s “Worst error, usually” box shows exactly this: 2.72 × 1,000 ÷ 16 ≈ 170, and 98.2% of the time. It is a limit, not a typical result: 98.2% of the time the error stays under 170. The errors you actually saw were much smaller, because the smallest of 4 counters is usually one of the lucky ones.
Guess first: After 10,000 posts, which count is more trustworthy: #sakura (the most popular tag) or #onsen (a rare one)?
#sakura. The extra comes from other tags sharing the counters, so it doesn’t grow with the tag’s own count. Say both pick up 30 extra: on #sakura’s roughly 2,700 posts that is about 1% off; on #onsen’s roughly 60 it is 50% off.
Solving the puzzle
Now size it. We want the error under 50,000, which is 1 in 10,000 of our 500 million posts, and we want to be at least 99% sure.
- Width: 2.72 × 500,000,000 ÷ 50,000 ≈ 27,200 counters per row. (Libraries use the exact e, 2.71828…, which gives 27,183.)
- Rows: 4 rows give 98.2%, just short. With 5 the chance of a bigger error is 0.368⁵ ≈ 0.007, so 99.3% of counts are within 50,000.
- Memory: 27,183 × 5 = 135,915 counters. A 4-byte counter holds up to about 4.3 billion, plenty for 500 million posts. That is 543,660 bytes, about 544 KB.
| Error allowed (of 500 million) | Counters per row | Rows | Memory |
|---|---|---|---|
| 500,000 (1 in 1,000) | 2,719 | 5 | 54 KB |
| 50,000 (1 in 10,000) | 27,183 | 5 | 544 KB |
| Exact (a map) | one per hashtag | – | 1.2 GB and growing |
One gap remains. The sketch can answer “how many #sakura?” but it can’t list hashtags, because it never stored their names. So next to it keep a small list of the current top 10, with names. For each post, add it to the sketch and read its estimate. If the tag is already on the list, update its number; if not, and it beats the smallest number on the list, it replaces that one.
That is the full answer: about 544 KB of counters plus a list of 10 names, instead of 1.2 GB.
It also spreads across servers. If every server uses the same width, rows and hashes, #sakura lands on the same counters everywhere. So adding two grids counter by counter gives exactly the grid one server would have built from both streams. Each server counts its own posts, and a central job adds the grids up.
The code
Notice that add and estimate pick the same counters; one bumps them, the other takes the smallest.
width = 27183
rows = 5
grid = [[0] * width for each row]
counter(row, tag):
return hash(row + ":" + tag) % width
add(tag):
for row in 0 .. rows-1:
grid[row][counter(row, tag)] += 1
estimate(tag):
return min(grid[row][counter(row, tag)] for row in 0 .. rows-1)Where you will meet it
- Redis has it built in: CMS.INCRBY adds, CMS.QUERY reads, CMS.MERGE combines sketches, and CMS.INITBYPROB sizes one from the error and certainty you want.
- Apache Spark builds one over a DataFrame column with df.stat.countMinSketch, to find frequent values without a full group-by.
- “Top K videos” and “trending now” designs use the sketch-plus-top-list pattern above.
- Abuse detection: count requests per IP address or API key to spot the few sending far more than everyone else.
Remember
- A few rows of shared counters; each item bumps one counter per row.
- Read the smallest of its counters: it can overcount, never undercount.
- Width sets how big the error is; rows set how rarely it happens.
- Memory depends on the error you accept, as a share of all items, not on how many different items there are.
- Good for the popular items, useless for rare ones.
See it in a design
- Top-K & leaderboards: Exact leaderboards in Redis sorted sets and approximate trending lists from a count-min sketch.
- Ad click aggregation: Log-then-count clicks with an exactly-once stream aggregator and a raw lake for batch recounts.
- Rate limiter: Token buckets in Redis checked at the gateway, so no single client can overwhelm the API.
Further reading and videos
- What is a Count-Min Sketch? (Standarity)
- Count-Min Sketch: An efficient probabilistic Data Structure (Devoxx UK (Raphael De Lio))
- Count min sketch | Efficient algorithm for counting stream of data (Tech Dummies)
- Data Structures for Big Data in Interviews - Bloom Filters, Count-Min Sketch, HyperLogLog (Hello Interview)
- Count-Min Sketch (Florian Hartmann)
- Count-Min Sketch: the art and science of estimating stuff (Redis blog)
- Design YouTube’s Top K Videos feature (Hello Interview)
- Count-min sketch (Redis docs)
- Count–min sketch (Wikipedia)
- An improved data stream summary: the count-min sketch and its applications (Cormode & Muthukrishnan (2005))