Leaderboards look like the easiest feature in a multiplayer game: a sorted list of numbers. Teams routinely build them straight onto the primary database, ship, and then discover the problem some months later, when the board gets big and the "what rank am I?" query starts dominating their database load.
The query that breaks
Showing the top of the board is easy. Fetching the top 100 scores is a single indexed query and stays fast forever.
The expensive query is the one every player triggers by opening the screen: where do I rank? On a relational table, answering that means counting every row with a better score:
SELECT COUNT(*) + 1 AS rank
FROM scores
WHERE score > (SELECT score FROM scores WHERE player_id = $1);
Even with an index on score, this is an index scan proportional to the number
of players ranked above you. At ten thousand players nobody notices. At a million,
the average player is scanning hundreds of thousands of index entries, every time
they open the screen, while the same database is trying to serve your actual
gameplay writes. Window functions (RANK() OVER (ORDER BY score DESC)) are worse,
because they rank the entire set to answer a question about one row.
Caching does not save you either, because rank is not cacheable per player: any score write anywhere in the board can change any other player's rank.
What Redis sorted sets change
A Redis sorted set (ZSET) keeps members ordered by score in a skip list with a
companion hash table. That structure gives the two operations you need in
logarithmic time regardless of board size:
ZADD leaderboard:global GT 48200 player:8f21 # write a score
ZREVRANK leaderboard:global player:8f21 # what rank am I -> O(log N)
ZREVRANGE leaderboard:global 0 99 WITHSCORES # top 100 -> O(log N + M)
ZREVRANK is the whole reason to reach for Redis here. Rank lookup goes from
"proportional to how many players beat you" to "proportional to the logarithm of
the board size". At a million entries that is roughly twenty steps instead of
hundreds of thousands, and it stops competing with your gameplay database for I/O.
The GT flag (Redis 6.2 and later) is worth knowing: it updates the score only
if the new value is greater than the stored one. That gives you high-score
semantics in a single atomic command, with no read-modify-write race between two
concurrent match results for the same player.
The details that decide whether it holds up
The data structure is the easy half. These are the parts that bite in production.
Authoritative scores belong in your primary database, and Redis is only the ranking view. It should be rebuildable from the database, because at some point you will need to rebuild it after a failover, a bad deploy or a migration. Design the rebuild path on day one and test it. A leaderboard you cannot reconstruct will one day be wrong with no way back.
Ties need a deterministic rule. Two players on 48,200 points will otherwise swap positions between requests, which players notice and report as a bug. The standard fix is to fold the tiebreaker into the score itself: take the score into the high bits and subtract a normalised timestamp in the low bits, so an earlier achievement outranks a later identical one. Doubles hold 53 bits of integer precision exactly. That is enough for a sensible split but not enough to be careless about, so decide the bit budget deliberately.
Give each season its own key, such as leaderboard:season:2026-q3, instead of a
season column, and set a TTL slightly beyond your archival job. Seasonal resets
become a key change rather than a mass delete, and historical boards stay
queryable until you have safely persisted them.
Persistence needs a decision. Default RDB snapshotting can lose the window since
the last snapshot on a hard failure. If your rebuild-from-database path is solid
and fast, that is an acceptable trade. If it is not, enable AOF with everysec
and accept the throughput cost. What you should not do is leave it unexamined and
assume the data is safe.
Memory use is predictable, but measure it anyway. Large sorted sets cost roughly 100 to 150 bytes per member once past the small-set listpack encoding, so a million players comes to about 100 to 150 MB per board. That is cheap, but multiply it by per-region, per-mode and per-season boards before sizing the instance.
Sharding is usually the wrong first move. A single sorted set handles tens of millions of members comfortably. If you do need to split, shard by segment (region, tier) rather than trying to shard one global ranking. Cross-shard global rank requires either an approximation or a coordinator, and both add considerably more complexity than most games need.
Keeping cheaters off the board
A fast leaderboard that ranks fabricated scores is worse than no leaderboard, because it tells your honest players that cheating works.
Scores must be produced by a server that simulated the match. If the client computes the score and posts it, the board will be forged within days of anyone caring about it, and no amount of request signing fixes that: the client owns the number before it signs it. This is the main reason we push teams toward server-authoritative match simulation early, even when the game ships with listen servers first. Retrofitting authority into client-authoritative gameplay code is close to a rewrite, and the leaderboard is where that cost shows up.
Beyond authority, budget for the basics: rate limiting on submission endpoints, plausibility bounds on scores, and a moderation path to remove entries and recompute, because you will need one.
Where this sits in the wider backend
Leaderboards are usually the first live service a team builds and the one that teaches the shape of the rest. The same pattern repeats for matchmaking queues, session state and player inventories: authoritative state in the primary store, a purpose-built structure serving the hot query, and a rebuild path between them.
We build this layer for Unreal Engine 5 and Unity titles: dedicated server builds orchestrated on demand, matchmaking and session flow, Redis-backed leaderboards, and the player persistence underneath. A common way to start is a scoped audit of one thing already misbehaving: desync, matchmaking latency, server spend, or a board that falls over at scale.
Still choosing an engine? Read Unreal Engine 5 or Unity? Choosing before you hire an external team.