Designing a Real-Time Leaderboard: Sorted Sets, Sharded Top-K, and Rank Queries
Leaderboards look like a toy question and hide a sharp asymmetry. "Show the top 10" is cheap at any scale — the head of the distribution is small, hot, and cacheable. "Show my rank" — for player 217,384,922 of 300 million, updating live as everyone scores — is a different computational object entirely: a global order statistic over a write-heavy set. Interviewers pick this question to watch whether you notice the asymmetry and split the design along it.
Requirements
Game with 300M players; scores update at ~50K writes/sec (spiky around events); queries: top-N (heavily read), player's rank + neighbors ("you're #1,247,332 — here are the 5 above/below you"), percentile tiers; freshness within seconds; weekly/seasonal resets.
The single-node answer: sorted sets
Up to tens of millions of members, this is Redis's flagship use case. A sorted set (ZSET) is a hash map + skip list: O(log N) insert/update, O(log N + K) range reads, and — the underrated part — O(log N) rank lookup (ZRANK), because skip-list nodes carry span counts, turning "position in order" into a sum along the search path.
ZADD lb:s42 15200 player:88 # update score
ZREVRANGE lb:s42 0 9 WITHSCORES # top 10
ZREVRANK lb:s42 player:88 # my rank
ZREVRANGE lb:s42 (r-3) (r+3) # neighbors around my rank
One node at 50K ops/sec is comfortable; memory (~100-150 bytes/member with IDs) puts 300M members at ~40GB — over one box's comfort line, and one partition's failure domain. So the real design starts where ZADD stops being the whole answer.
Sharding: and the rank query breaks
Shard players across M sorted sets by hash(player_id). Writes stay O(log n) local — perfect. Reads split by the asymmetry:
Top-N: fetch top-N from each shard, merge — a K-way merge of M sorted lists, costing M×N reads for exact results. But since top-N is read millions of times and changes slowly relative to reads, the answer is a materialized head: a background process (or write-through on any score entering head range) maintains a single small "top-1000" set that all top-N reads hit. The head is hot, tiny, and replicated — reads never fan out.
Rank: my_rank = Σ over shards of count(score > mine) — ZCOUNT per shard is O(log n), so an exact rank costs M small queries. At M=16 that's fine per-query but multiplies under load, and this is where the senior move appears: nobody needs their 9th digit of rank to be exact. Approximate the tail:
maintain a global score histogram (e.g., 10K buckets, updated by stream):
rank ≈ Σ counts of buckets above mine + position within my bucket (interpolated)
exact for head (top 1000 via materialized set), approximate beyond
error: ±0.01% of population — invisible at rank 1,247,332
One histogram read replaces M shard queries; the histogram updates from the score stream and is periodically rebuilt for drift. Exact-head + approximate-tail is the same "precision where eyes are" budget as ad-click counting, and stating it as an explicit product decision — ranks past 1000 are estimates — is the answer that separates levels.
The write path grows up
At event spikes (10× writes, everyone finishing a match at once), score updates go through a small pipeline rather than raw ZADDs: a stream (Kafka/Kinesis) absorbs the spike; consumers apply conflation — multiple updates for one player within a window collapse to the last/best (leaderboards care about current standing, not history, so intermediate values are droppable — say why conflation is legal here); durable score-of-record lands in a database (the ledger for audits and rebuilds), with sorted sets as the rebuildable serving view. That last sentence carries the recovery story: a lost shard is repopulated from the database + stream replay, not mourned.
Anti-cheat sits in this pipeline too — validation before the score enters the set (server-authoritative scoring, bounds checks, statistical outlier holds) — because removing a fraudulent #1 from a live leaderboard is far messier than never admitting it.
Resets, seasons, and variants
Weekly reset ≠ DEL on a 40GB key (that's a multi-second blocking stall — use UNLINK/lazy free). Cleaner: leaderboards are named by window (lb:2026w29) — new week, new empty set, old ones age out by TTL/archival; "all-time" is just another window. Percentile tiers ("top 5% = Diamond") read off the same histogram as approximate rank. Friends-leaderboards invert the problem: tiny sets (~200 members), computed per request from friends' scores — no global order needed, no shard fan-out; the same query over a different graph is nearly free (the nearby-friends lesson again: fan-out cost is a property of the graph, not the feature).
| Interview probe | Answer sketch |
|---|---|
Why not ORDER BY score DESC in SQL? | 50K writes/sec of index churn + rank = offset scan (O(N)); B-trees don't carry span counts |
| Two players, same score? | Composite score (points << 20) - timestamp — deterministic tiebreak inside one sort key, no second sort dimension |
| Score update lost? | Stream is the source; ZSETs rebuildable; conflation makes replay idempotent-enough (last-write per player) |
| Global + per-region boards? | Write once to stream; consumers project into region/global/mode views — one fact, many materialized orders |
The shape worth keeping: an ordered index you can afford is a serving view; the truth is a stream + ledger behind it. Once ranks past the head are declared approximate, every scaling problem in this system becomes pleasantly ordinary.
Keep reading
Consistency Models Beyond CAP: Linearizability, Causal, and Session Guarantees
'Strong vs eventual' is a cartoon. The real spectrum — linearizable, sequential, causal, session guarantees — and how to pick per operation, not per system.
Designing an API Gateway: The Front Door as a System
Authentication, routing, rate limits, transformations, and a plugin chain — in a tier that must add ~1ms and never be the outage. Envoy/Kong architecture from scratch.
Designing a CDC Pipeline: Change Data Capture from Binlog to Downstream
Dual writes are a lie; the database's own log is the truth. Log-based CDC, the snapshot-plus-stream handoff, schema evolution, and ordering guarantees that survive resharding.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.