Redis

Real-Time Leaderboard with Redis Sorted Sets

June 9, 2026
6 min read

Leaderboards are deceptively hard. Sorting millions of scores on every request is expensive, and keeping rankings fresh in real time strains any SQL database. Redis Sorted Sets solve this elegantly — every operation that updates a score also updates the rank, in O(log N) time.

What Are Redis Sorted Sets?

A sorted set is a collection of unique members, each associated with a floating-point score. Redis keeps members sorted by score at all times. You can retrieve members by rank (position) or by score range, and update scores atomically.

Every leaderboard operation maps directly to a sorted set command:

  • Add/update scoreZADD
  • Get player rankZREVRANK
  • Get top-N playersZREVRANGE
  • Increment scoreZINCRBY
  • Get scoreZSCORE

Basic Leaderboard Operations

# Add players with scores
ZADD leaderboard:global 5000 "alice"
ZADD leaderboard:global 8200 "bob"
ZADD leaderboard:global 7100 "carol"
ZADD leaderboard:global 9500 "dave"

# Get top 3 players (highest scores first) with scores
ZREVRANGE leaderboard:global 0 2 WITHSCORES
# 1) "dave"  2) "9500"
# 3) "carol" 4) "7100"
# 5) "bob"   6) "8200"

# Get alice's rank (0-indexed, lower is better rank)
ZREVRANK leaderboard:global "alice"   # returns 3 (4th place)

# Get alice's score
ZSCORE leaderboard:global "alice"     # returns "5000"

# Add 500 points to alice's score (atomic increment)
ZINCRBY leaderboard:global 500 "alice"

# Total number of players
ZCARD leaderboard:global

Python Implementation

import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)
LEADERBOARD = "leaderboard:global"

def add_score(player, score):
    r.zadd(LEADERBOARD, {player: score})

def increment_score(player, points):
    return r.zincrby(LEADERBOARD, points, player)

def get_rank(player):
    rank = r.zrevrank(LEADERBOARD, player)
    return rank + 1 if rank is not None else None  # 1-indexed

def get_top(n=10):
    top = r.zrevrange(LEADERBOARD, 0, n - 1, withscores=True)
    return [{"rank": i + 1, "player": p, "score": int(s)}
            for i, (p, s) in enumerate(top)]

def get_around_player(player, radius=2):
    rank = r.zrevrank(LEADERBOARD, player)
    if rank is None:
        return []
    start = max(0, rank - radius)
    end = rank + radius
    entries = r.zrevrange(LEADERBOARD, start, end, withscores=True)
    return [{"rank": start + i + 1, "player": p, "score": int(s)}
            for i, (p, s) in enumerate(entries)]

Time-Windowed Leaderboards

Daily, weekly, and all-time leaderboards are common requirements. Use separate keys per time window and let them expire automatically.

from datetime import datetime

def get_leaderboard_key(period='global'):
    today = datetime.utcnow()
    if period == 'daily':
        return f"leaderboard:{today.strftime('%Y-%m-%d')}"
    elif period == 'weekly':
        week = today.isocalendar()[1]
        return f"leaderboard:{today.year}:w{week}"
    return "leaderboard:global"

def record_score(player, points, period='global'):
    key = get_leaderboard_key(period)
    r.zincrby(key, points, player)
    # Auto-expire daily boards after 2 days
    if period == 'daily':
        r.expire(key, 172800)  # 48 hours
    elif period == 'weekly':
        r.expire(key, 604800 * 2)  # 2 weeks

Pagination

def get_leaderboard_page(page=1, page_size=20):
    start = (page - 1) * page_size
    end = start + page_size - 1
    entries = r.zrevrange(LEADERBOARD, start, end, withscores=True)
    return [{"rank": start + i + 1, "player": p, "score": int(s)}
            for i, (p, s) in enumerate(entries)]

Key Takeaways

  • Sorted sets maintain order automatically — no sorting needed at read time
  • ZINCRBY atomically increments scores — safe for concurrent score updates
  • ZREVRANK gives rank in O(log N) — instant regardless of leaderboard size
  • Separate keys per time window enable daily/weekly/all-time boards with automatic cleanup via EXPIRE
  • A leaderboard with 10 million players uses roughly 800MB of RAM — very manageable