Skip to content
rushikesh
Go back

Building a Real-Time Leaderboard with Redis

A leaderboard looks simple: store a score, sort the players, and show the top results. The hard part appears when millions of players update their scores while everyone expects their rank immediately.

A relational database can handle a small leaderboard, but continuously sorting a large table is expensive. Redis sorted sets are designed for exactly this access pattern.

Why a SQL-Only Leaderboard Gets Expensive

A basic SQL table might look like this:

CREATE TABLE leaderboard (
  player_id uuid PRIMARY KEY,
  score bigint NOT NULL DEFAULT 0,
  updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX leaderboard_score_idx ON leaderboard (score DESC);

Getting the top players is straightforward:

SELECT player_id, score
FROM leaderboard
ORDER BY score DESC
LIMIT 10;

The index makes this query fast, but frequent score updates also require frequent index updates. Calculating the rank of an arbitrary player becomes more expensive as the table grows:

SELECT COUNT(*) + 1 AS rank
FROM leaderboard
WHERE score > (SELECT score FROM leaderboard WHERE player_id = $1);

At high write rates, the database spends significant work maintaining ordering and answering rank queries.

Redis Sorted Sets

A Redis sorted set stores unique members alongside numeric scores. Redis keeps the members ordered, so score updates, rank lookups, and range queries are efficient.

Use one key for a leaderboard and the player ID as the member:

ZADD leaderboard:global 1250 player:42
ZADD leaderboard:global 980 player:17
ZADD leaderboard:global 1560 player:91

The core operations are:

Updating Scores Atomically

Games commonly add points rather than replace the whole score. ZINCRBY performs that change atomically:

ZINCRBY leaderboard:global 25 player:42

Concurrent requests cannot overwrite each other’s increments. This avoids the read-modify-write race that appears when an application reads a value, increments it locally, and writes it back.

If the business rule is more complex, such as awarding points only once for an event, make idempotency part of the design. Record the event ID and update the score in a Lua script or transaction so retries do not award the same points twice.

Reading the Top Players

Return the top ten players with their scores:

ZREVRANGE leaderboard:global 0 9 WITHSCORES

Redis uses zero-based ranges, so 0 9 means the first ten entries. The reverse variant is important because sorted sets are ordered from the lowest score to the highest by default.

For pagination, convert the page into start and stop offsets:

# Page 3 with 20 players per page
ZREVRANGE leaderboard:global 40 59 WITHSCORES

Offset pagination works well for shallow pages. For very deep pagination, consider score-based windows because Redis still has to skip earlier entries.

Finding a Player’s Rank

ZREVRANK leaderboard:global player:42

The result is zero-based, so add one before displaying it to a user. Fetch the rank and score together with a pipeline to save a network round trip:

ZREVRANK leaderboard:global player:42
ZSCORE leaderboard:global player:42

A useful leaderboard response usually includes the global top players plus a small window around the current player. That gives users context even when they are ranked far below the first page.

Handling Ties

Redis orders members with equal scores lexicographically. That behavior is deterministic, but it might not match the product rule.

Common tie-breaking rules include:

If a secondary value must affect ordering, encode it into the numeric score carefully. For example, combine points with an inverted timestamp while staying within the integer precision that Redis scores can represent exactly. When correctness is more important than a compact encoding, store the primary rank in Redis and resolve ties from durable metadata in the database.

Seasons and Multiple Leaderboards

Separate leaderboards by key rather than deleting and rebuilding one global key:

leaderboard:global:2025-08
leaderboard:friends:2025-08:<player_id>
leaderboard:country:2025-08:IN

At the start of a new season, write to a new key. The previous key becomes an immutable archive and can expire later:

EXPIRE leaderboard:global:2025-08 7776000

Be careful with per-player friend leaderboards. Materializing one sorted set for every user can multiply storage dramatically. For smaller friend groups, fetch the members’ scores from the global set and rank them in the application.

Redis Is Fast, but It Should Not Be the Only Copy

Redis can persist data with AOF or snapshots, but a production leaderboard often needs an authoritative event or score history elsewhere. A practical architecture is:

game event -> durable database or event log -> score worker -> Redis sorted set
                                                     |
client API <- leaderboard service <------------------+

The durable store provides recovery and auditing. Redis serves the current ranking with low latency. If Redis is lost, replay score events or rebuild the sorted set from the database.

For stricter consistency, write the durable event first and update Redis asynchronously. The leaderboard may lag briefly, but acknowledged points are never lost. For games where immediate display matters more, update Redis synchronously and publish a durable event with a clear reconciliation strategy.

Scaling Beyond One Redis Node

One global sorted set lives on one Redis shard, so it cannot be divided transparently across a Redis Cluster. Most applications can support a surprisingly large leaderboard on one well-sized node, but truly global scale requires a different design.

Options include:

  1. Partition players into regional sorted sets and periodically merge the top candidates.
  2. Keep regional leaderboards for writes and maintain a separate global top set.
  3. Use approximate ranks when an exact global rank is not a product requirement.

Before adding this complexity, measure the real workload. Network calls, hot keys, and update frequency usually matter more than the theoretical number of registered players.

Practical Checklist

  1. Use ZINCRBY for atomic score increments.
  2. Define tie behavior before choosing a score encoding.
  3. Pipeline rank and score reads.
  4. Give each season its own key and expiration policy.
  5. Keep a durable source of truth for rebuilding Redis.
  6. Load-test the hottest leaderboard, not only average traffic.
  7. Monitor memory, command latency, replication lag, and hot-key traffic.

Redis sorted sets make the basic leaderboard operations simple. The real system-design work is deciding how scores become durable, how ties behave, and how much global consistency the product actually needs.

References


Share this post on:

Previous Post
Database Isolation Levels: Performance Impact and Trade-offs
Next Post
Garbage Collection 101: Understanding Memory Management