← All postsArticle

Leaderboards That Survive a Million Players: Redis Sorted Sets in a Game Backend

Why leaderboards built on a relational table fall over at scale, how Redis sorted sets fix the rank query, and the design details (ties, seasons, persistence, cheating) that decide whether it holds up in production.

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.

← Back to all postsWork with us