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.
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.