Designing Leaderboards at Scale
Designing leaderboards at scale: why a sorted database query dies under load, how a Redis sorted set fixes it, and how to shard ranking past one node.
Part of Distributed Systems Patterns That Hold Up in Production
Designing leaderboards at scale is a deceptively hard system design problem, because the obvious solution, a database query that sorts by score, is exactly the thing that dies under load. Leaderboards are write-heavy and rank-heavy at once, and a SELECT ... ORDER BY score recomputed on every read does not survive real traffic. The scalable answer is a purpose-built ranking structure, a sorted set, and a sharding plan for when one node is not enough.
It looks trivial until the numbers get big: millions of players, scores updating constantly, and everyone wanting to see the top 100 and their own rank instantly. That combination is what makes it a real interview question and a real production challenge.
Why leaderboards are harder than they look
A leaderboard has to do three things fast and at once: update a player’s score (constantly), return the top-N (constantly), and return a specific player’s rank (constantly). Each of those is cheap alone. Together, at scale, on changing data, they fight each other.
The naive design treats the leaderboard as a query problem and reaches for the database. That works at small scale and falls over at large scale, for reasons that are worth understanding before you pick the alternative. This post is part of the Distributed systems patterns series.
Why not just use SQL ORDER BY for a leaderboard?
Because a leaderboard is the worst case for a sorted query: the data is large, the reads want it fully ordered, and the writes constantly change the ordering. Running ORDER BY score LIMIT 100 over millions of rows is expensive per read, and maintaining the sort index under a heavy write stream adds its own cost. Computing a single player’s exact rank is worse, it is effectively a count of everyone above them.
The specific pain points stack up:
- Top-N reads sort or scan a large index repeatedly.
- Rank-of-player requires counting all higher scores, an expensive operation.
- Constant score updates churn the index, adding write amplification.
- Hot rows (the top of the board) get read enormously.
A relational database can serve a small leaderboard fine. It is the combination of scale, write rate, and rank queries that makes ORDER BY the wrong tool here.
How does a Redis sorted set power a leaderboard?
A sorted set stores each member with a score and keeps them ordered automatically, supporting score updates, rank lookups, and range (top-N) queries in logarithmic time. You update a player’s score on each scoring event, and you read the top-N or any player’s rank directly, with no full sort and no counting pass. That logarithmic, no-resort behavior is exactly what a leaderboard needs.
The common operations map cleanly: add or update a score on each event, fetch a descending range for the top-N, and look up a member’s rank for “your position.” Because these are all native, fast operations, a single sorted set comfortably handles a leaderboard far larger than a database ORDER BY could serve, and it does so under continuous score updates.
How do you shard a leaderboard that is too big for one node?
First, avoid a single giant leaderboard if the product allows it: partition by segment, per region, per league, per game mode, or per time window (daily, weekly, seasonal), so each segment is an independent, smaller leaderboard that fits comfortably. Most “global” leaderboards are actually better as many scoped ones, and that segmentation is also a better product.
When you genuinely need one ranking larger than a single node can hold, the pattern is per-shard sorted sets plus a merge:
- Partition members across shards (by hash or range).
- Each shard maintains its own sorted set.
- For top-N, fetch the top-N from every shard and merge them; the global top-N is contained in the union of per-shard top-Ns.
- For exact global rank, you must query all shards, which is expensive, so most systems serve exact ranks only for the top tier and approximate ranks below it.
The honest tradeoff is that exact global ranking across shards is costly, and at scale it is usually unnecessary. Players care intensely about the top of the board and about their own neighborhood; an approximate rank in the long tail is almost always acceptable, and far cheaper.
Keeping the leaderboard consistent and abuse-resistant
Two production concerns ride along with leaderboards. First, score updates should be idempotent: a retried score event must not double-count, which means keying updates by event ID, the discipline from Idempotency Keys for Distributed Systems. Second, leaderboards attract cheating, so the score-submission path needs validation and rate limiting, because a leaderboard is a public, high-value target for manipulation.
Persistence matters too. An in-memory sorted set is fast but volatile, so you back it with durable storage: treat the database as the source of truth for scores and the sorted set as the fast ranking index, rebuildable from the source. That separation gives you both speed and durability without asking one system to do both jobs.
How do you show a player their rank and neighbors?
Use the sorted set’s rank lookup to find the player’s position, then read the small range around it for the “players near you” view. Getting a member’s rank and fetching the few entries above and below it are both fast, native operations, so the common “you are rank 1,240, here are the players just ahead of you” panel costs almost nothing. This is the feature players actually care about, and the structure makes it cheap.
The contrast with the database approach is stark. In SQL, finding a player’s rank means counting every higher score, and showing their neighbors means an offset query deep into a sorted result, both expensive on large data. With a sorted set, rank-of-member and a bounded range query around that rank are direct operations, so the per-player view scales as well as the top-N view does.
This is why the sorted set is such a good fit beyond just the top-N: it serves all three of the queries players make, the global top, your own rank, and the players around you, with the same fast primitives. The design that started as “show the top 100” turns out to handle “show me where I stand” just as gracefully, which is usually the more engaging feature.
A leaderboard design checklist
Before you ship a leaderboard at scale:
- Ranking uses a sorted set (or equivalent), not a database
ORDER BYon every read. - The board is segmented (region/league/time window) wherever the product allows, to keep each one small.
- For a true global board too big for one node, you have a per-shard merge plan and accept approximate tail ranks.
- Score updates are idempotent, keyed by event ID.
- The submission path validates and rate-limits to resist cheating.
- The fast ranking index is backed by durable storage and is rebuildable.
- Hot reads (the top of the board) are cached or served from the in-memory structure.
How do you handle ties and rank semantics?
Ties are the detail that turns a working leaderboard into a correct one, and they are almost always discovered in production rather than in design.
The first question is what rank means when scores are equal. Competition ranking gives tied players the same rank and skips the next values — 1, 2, 2, 4 — which is what most games and sports expect. Dense ranking gives 1, 2, 2, 3, leaving no gaps. Ordinal ranking breaks ties arbitrarily so every player has a distinct rank. Pick one deliberately, because switching later changes every displayed number and users notice.
Sorted-set implementations rank by score and then by member key lexicographically, which is ordinal ranking with an arbitrary and — worse — stable and guessable tiebreak. Players with names early in the alphabet permanently outrank equal-scoring players later in it, which is unfair in a way that becomes visible the moment anyone looks.
The usual fix is a composite score encoding the tiebreak into the sort value: combine the score with an inverted timestamp so that among equal scores, whoever reached it first ranks higher. This is a genuinely defensible rule, it is stable, and it removes the alphabetical artefact. The mechanics matter — the timestamp component must be inverted and scaled so it never overflows into the score component, which is worth testing explicitly because a subtle overflow silently corrupts ordering for the highest scorers.
Two related decisions. Decide whether scores can decrease, since a leaderboard that only accepts improvements is far simpler — updates are idempotent, out-of-order writes are harmless, and replaying events cannot damage state. And decide what happens to inactive players: a seasonal reset keeps the board relevant and bounded, while an all-time board grows forever and becomes progressively less interesting to new players who cannot realistically enter the top ranks.
How do you keep a leaderboard honest?
A leaderboard is a target, and anything that ranks users publicly will be attacked. The integrity work is not optional and it is easiest to build in from the start.
Never trust a client-submitted score. The score must be computed or validated server-side from events the server observed. A client that reports its own result will eventually report an impossible one, and by the time you notice, the board is polluted and removing entries is a visible, unpopular action.
Bound what is physically possible. Rate limits on submissions, sanity ceilings on score deltas, and minimum plausible durations for an achievement catch the crude attacks cheaply. Most cheating is not sophisticated; it is a replayed request or an implausible value that a single validation rule would have rejected.
Make submissions idempotent. A retried submission must not count twice — the idempotency-key discipline applied to scoring. Without it, a network retry inflates a score accidentally and looks identical to cheating, which wastes investigation time.
Keep an audit trail. Store the events that produced a score, not just the score. Without them you cannot investigate a suspicious entry, cannot recompute after fixing a bug, and cannot justify a removal to the user affected.
Separate detection from enforcement. Flag suspicious entries into a review queue rather than removing them automatically. False positives on a public ranking are extremely damaging to trust, and an automated system that occasionally deletes legitimate achievements does more harm than the cheating it prevents.
The design property that makes all of this tractable: the leaderboard should be derived state, not source of truth. If it is a projection over an event log, you can recompute it entirely after finding a bug or removing a cheater, rather than surgically editing a live ranking and hoping the arithmetic is right. That is a strong reason to store scoring events durably even when the sorted set is what serves reads.
What does a leaderboard cost at scale?
The design choices have cost consequences that are easy to miss until the bill arrives, and the dominant cost is usually not what people expect.
Reads dominate, overwhelmingly. Every player checking their rank is a read; scores are written comparatively rarely. So the optimisation target is read cost, and the highest-leverage move is caching the top-N page — which is what almost everyone requests, is identical for all users, and changes slowly enough that a few seconds of staleness is imperceptible.
Per-player rank lookups are the expensive query. “Where am I” cannot be served from a shared cache because it differs per user. In a sorted set the rank operation is logarithmic and fine at moderate size, and it becomes a real cost at very large N multiplied by every player checking frequently. Two mitigations: cache each player’s rank briefly per user, and consider serving an approximate rank — “top 5%” — which is cheaper to compute, arguably more meaningful to the player, and much less sensitive to churn.
Memory is the constraint for in-memory implementations. A sorted set holds every member and score in RAM. Tens of millions of players is a substantial memory footprint, and it is the reason boards are usually bounded — by season, by region, or by keeping only the top N with everyone else served an approximate standing.
The architectural conclusion: most leaderboards do not need a globally accurate ranking for every player. They need an accurate top-N, an accurate rank for the player asking, and a plausible sense of position for everyone else. Designing to that requirement rather than to a naive “rank everyone precisely, always” is what keeps both the cost and the complexity bounded — and users cannot tell the difference, because nobody is checking whether they are 84,213th or 84,220th.
How do you update a leaderboard reliably?
The write path is where correctness is decided, and there are two workable architectures with different trade-offs.
Synchronous update on score change. The service writing the score also updates the ranking structure, ideally in the same transaction or immediately after. Simple, immediately consistent, and it couples your scoring path’s availability to the leaderboard store — if the ranking store is slow, scoring is slow.
Asynchronous update via events. The scoring service emits an event; a consumer updates the ranking. Decoupled, absorbs bursts, and the leaderboard becomes eventually consistent by a small margin — which is almost always acceptable, since nobody can distinguish a rank that is two seconds stale.
The asynchronous model is the better default at scale, for the same reason it is elsewhere: it isolates a bursty, less-critical projection from the critical path that produces it. It also makes the leaderboard genuinely rebuildable, since the events exist independently of the ranking structure.
Two correctness requirements apply regardless of which you choose. The update must be idempotent, so a retried event does not double-apply — and the natural way to achieve this is to set an absolute score rather than increment, which converges no matter how many times it is applied. And it must tolerate out-of-order delivery, refusing to overwrite a higher or newer score with an older one, since an event replay or a partition can deliver stale updates after fresh ones.
Those two together — absolute writes and a monotonic guard — mean the ranking converges to the correct state under retries, reordering, and replays, without any distributed coordination. That property is worth more than it sounds: it makes the entire leaderboard safe to rebuild from history at any time, which is the recovery path for every bug you will subsequently find in the scoring logic.
Which storage should back a leaderboard?
The access pattern is narrow — insert or update a score, read the top N, read one member’s rank — and that narrowness makes the choice fairly determined.
| Option | Fits when | Watch out for |
|---|---|---|
| In-memory sorted set | The default; excellent for top-N and rank queries | Memory bounds total members; durability needs configuring |
| Relational table with an index | Small boards, or you already run the database | Rank queries require counting rows and degrade badly with size |
| Wide-column store | Very large boards, time-bucketed or sharded | Rank across the whole set is expensive; suits top-N per partition |
| Search or analytics engine | Complex filters — by region, by cohort, by time window | Heavier and slower to update; near-real-time rather than real-time |
The relational row is the one worth being direct about. SELECT COUNT(*) WHERE score > ? to compute a rank is fine at ten thousand rows and unusable at ten million, because it scans. Teams reach for it because the database is already there, and it works until the board is popular — which is precisely when it stops working, and the migration happens under pressure.
Start with a sorted set unless you have a specific reason not to. It is purpose-built for exactly this shape, the operations are logarithmic, and the operational model is well understood. Add durability deliberately — persistence configured, and the ability to rebuild from the event log — because an in-memory structure that is your only copy of the ranking is a single restart away from being an incident.
For genuinely enormous boards, the pattern that scales is sharding by segment with a merged view: rank within region, cohort, or time bucket, then merge the top of each for a global view. This keeps every individual structure small, makes most queries answerable from one shard, and accepts that a precise global rank for the 400,000th player is a requirement nobody actually has.
What I’d do differently
The trap I would flag is building a single global, exact, all-time leaderboard because it sounds impressive, when the product would be better served by scoped, time-boxed boards that are also far cheaper to run. The hardest version of this problem is often one you do not actually need to solve.
If I were designing a leaderboard again, I would start by questioning the requirement: does this need to be global and exact, or would per-league weekly boards serve players better and scale trivially? When a true global ranking is genuinely required, I would use a sorted set per shard with a top-N merge and exact ranks only where they matter, rather than chasing exact global rank for every player at every position. Match the engineering to what players actually feel, which is the top of the board and their own position, not the precise rank of the 4-millionth player.
Sources
- Redis, Sorted sets: redis.io/docs/latest/develop/data-types/sorted-sets
- The System Design Primer: github.com/donnemartin/system-design-primer
- Redis, solutions and patterns: redis.io/solutions
Frequently asked questions
How do you build a scalable leaderboard?
Use a data structure built for ranking rather than a sorted SQL query. A Redis sorted set stores members by score and returns rank and top-N in logarithmic time, so updates and reads stay fast under heavy write load, which is exactly where a database ORDER BY query falls apart.
Why not just use SQL ORDER BY for a leaderboard?
Because ranking millions of rows by score on every read is expensive, and leaderboards are write-heavy, so the sorted index is constantly churning. Under real load the ORDER BY plus index maintenance becomes a bottleneck. A sorted set keeps both updates and rank lookups fast.
How does a Redis sorted set power a leaderboard?
A sorted set keeps members ordered by a score and supports add/update, rank lookup, and range queries (top-N) in logarithmic time. You update a player's score on each event and read the top-N or a player's rank directly, with no full sort, which is what makes it fast at scale.
How do you shard a leaderboard that is too big for one node?
Partition by segment when you can (per region, per league, per time window), so each shard is an independent leaderboard. For a single global ranking too large for one node, maintain per-shard sorted sets and merge the top-N from each shard, accepting approximate global ranks below the top.
How do you handle ties in a leaderboard?
Choose competition, dense, or ordinal ranking deliberately, since switching later changes every displayed rank. Sorted sets break ties lexicographically by member key, which permanently favours names early in the alphabet. Encode an inverted timestamp into a composite score so earlier achievers rank higher.
How do you stop cheating on a leaderboard?
Never trust client-submitted scores, bound what is physically possible with rate limits and sanity ceilings, make submissions idempotent so retries do not inflate scores, keep an audit trail of the events behind each score, and flag suspicious entries for review rather than removing them automatically.
What is the expensive part of running a leaderboard?
Reads, not writes. The top-N page is identical for everyone and should be cached. Per-player rank lookups cannot share a cache and are the real cost at scale; cache them briefly per user or serve an approximate rank such as "top 5%", which is cheaper and arguably more meaningful.
Should leaderboard updates be synchronous or event-driven?
Event-driven is the better default at scale, since it isolates a bursty projection from the scoring critical path and makes the board rebuildable from history. Either way, updates must set an absolute score rather than increment so retries converge, and must refuse to overwrite a newer score with an older one.
What database should back a leaderboard?
An in-memory sorted set by default, since the operations are logarithmic and purpose-built for top-N and rank queries. Avoid computing rank with a relational COUNT over rows, which scans and degrades badly past a few million entries. For very large boards, shard by segment and merge the top of each.