{
  "id": "redis-py",
  "title": "Redis leaderboard with redis-py",
  "url": "https://redis.io/docs/latest/develop/use-cases/leaderboard/redis-py/",
  "summary": "Implement a Redis leaderboard in Python with redis-py and sorted sets",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc"
  ],
  "last_updated": "2026-04-28T13:01:52-05:00",
  "children": [],
  "page_type": "content",
  "content_hash": "90071b3baf32662c8dd30d7d91f616e2ce7f939e3511d193c5c2d8ce731b8ff0",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This guide shows you how to implement a Redis-backed leaderboard in Python with [`redis-py`](https://redis.io/docs/latest/develop/clients/redis-py). It uses a sorted set to store rank order, Redis hashes to store per-user metadata, and a small local web server so you can explore the leaderboard interactively in your browser."
    },
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Leaderboards are one of the classic Redis use cases. A sorted set stores each member together with a numeric score, and Redis keeps the members ordered by that score for you.\n\nThat gives you:\n\n* Fast score updates for existing users\n* Simple top `n` leaderboard queries\n* Efficient queries for entries around a specific rank position\n* Straightforward trimming to a fixed leaderboard size\n* A clean separation between rank data and richer user metadata\n\nIn this example, the leaderboard score data is stored in a sorted set called `leaderboard:demo`, and each user's metadata is stored in a hash such as `leaderboard:demo:user:player-17`."
    },
    {
      "id": "how-it-works",
      "title": "How it works",
      "role": "content",
      "text": "The flow looks like this:\n\n1. Store each user ID in a sorted set with their score\n2. Store per-user metadata in a separate Redis hash keyed by user ID\n3. Fetch the highest-ranked users with a reverse range query\n4. Fetch users around a given rank by calculating a rank window\n5. Trim the leaderboard after updates so only the top configured entries remain\n\nSeparating rank data from metadata keeps the leaderboard operations efficient while still letting you render richer profiles in the application."
    },
    {
      "id": "the-python-leaderboard",
      "title": "The Python leaderboard",
      "role": "content",
      "text": "The `RedisLeaderboard` class wraps the common leaderboard operations\n([source](leaderboard.py)):\n\n[code example]"
    },
    {
      "id": "data-model",
      "title": "Data model",
      "role": "content",
      "text": "The implementation uses two Redis structures:\n\n[code example]\n\nThe score data lives in the sorted set, while the user details live in hashes keyed by the same user ID.\n\nThe implementation uses:\n\n* [`ZADD`](https://redis.io/docs/latest/commands/zadd) to add or update leaderboard scores\n* [`ZRANGE`](https://redis.io/docs/latest/commands/zrange) with the `REV` option to fetch the highest-ranked members\n* [`ZREVRANK`](https://redis.io/docs/latest/commands/zrevrank) to find a user's rank from the top\n* [`ZREMRANGEBYRANK`](https://redis.io/docs/latest/commands/zremrangebyrank) to trim the lowest-ranked overflow entries\n* [`HSET`](https://redis.io/docs/latest/commands/hset) and [`HGETALL`](https://redis.io/docs/latest/commands/hgetall) to store and load user metadata\n* [`DEL`](https://redis.io/docs/latest/commands/del) to remove metadata for trimmed or deleted users"
    },
    {
      "id": "leaderboard-implementation",
      "title": "Leaderboard implementation",
      "role": "content",
      "text": "The `upsert_user()` method writes the score, updates metadata, and then trims the board if it exceeds the configured limit:\n\n[code example]\n\nTo fetch users around a rank, the implementation converts the requested rank and count into a reverse range window:\n\n[code example]\n\nIf you need stronger guarantees for highly concurrent production writes, you can move the update-and-trim behavior into a Lua script or a tighter transaction flow. For a small application or demo, this Python implementation is easy to follow and works well."
    },
    {
      "id": "metadata-design",
      "title": "Metadata design",
      "role": "content",
      "text": "The leaderboard stores only user IDs and scores in the sorted set. Richer details stay in a separate per-user hash. That means the same user ID can be ranked efficiently while still exposing extra fields such as:\n\n* Display name\n* Short description\n* Team or country\n* Avatar URL\n* Other lightweight profile fields\n\nThis is a useful pattern when the ranking view and the profile view need different data shapes."
    },
    {
      "id": "prerequisites",
      "title": "Prerequisites",
      "role": "content",
      "text": "Before running the demo, make sure that:\n\n* Redis is running and accessible. By default, the demo connects to `localhost:6379`.\n* The `redis` Python package is installed:\n\n[code example]\n\nIf your Redis server is running elsewhere, start the demo with `--redis-host` and `--redis-port`."
    },
    {
      "id": "running-the-demo",
      "title": "Running the demo",
      "role": "content",
      "text": "A local demo server is included to show the leaderboard in action\n([source](demo_server.py)):\n\n[code example]\n\nThe demo server uses only Python standard library features for HTTP handling:\n\n* [`http.server`](https://docs.python.org/3/library/http.server.html) for the web server\n* [`urllib.parse`](https://docs.python.org/3/library/urllib.parse.html) for form decoding and query parsing\n* [`json`](https://docs.python.org/3/library/json.html) for API responses\n\nIt exposes a small interactive page where you can:\n\n* Add or update a player score and metadata\n* Increase a player's score incrementally\n* View the top `n` players on the leaderboard\n* View the `n` players around a chosen rank\n* Change the maximum number of entries the leaderboard keeps\n* Reset the demo dataset to a known starting state\n\nAfter starting the server, visit `http://localhost:8080`."
    },
    {
      "id": "production-usage",
      "title": "Production usage",
      "role": "content",
      "text": "This guide uses a deliberately small local demo so you can focus on the Redis leaderboard pattern. In production, you will usually want to add more validation, tighter concurrency control, and application-specific lifecycle rules."
    },
    {
      "id": "decide-how-ties-should-behave",
      "title": "Decide how ties should behave",
      "role": "content",
      "text": "Redis sorted sets order primarily by score. When two members have the same score, Redis uses the member value as a secondary ordering rule. If your application needs a different tie-breaker, you may want to encode it in the score or store additional state."
    },
    {
      "id": "consider-how-you-expire-or-archive-old-data",
      "title": "Consider how you expire or archive old data",
      "role": "content",
      "text": "Some leaderboards are permanent, while others reset daily, weekly, or seasonally. Depending on your use case, you may want to:\n\n* Namespace keys by season or event\n* Snapshot historical results elsewhere\n* Rebuild the current leaderboard from upstream data"
    },
    {
      "id": "keep-metadata-lightweight",
      "title": "Keep metadata lightweight",
      "role": "content",
      "text": "Per-user hashes work best for small, frequently accessed profile details. Large profile documents or rarely used attributes are often better kept in another store, with Redis holding only the fields needed to render the leaderboard quickly."
    }
  ],
  "examples": [
    {
      "id": "the-python-leaderboard-ex0",
      "language": "python",
      "code": "import redis\nfrom leaderboard import RedisLeaderboard\n\nr = redis.Redis(host=\"localhost\", port=6379, decode_responses=True)\nboard = RedisLeaderboard(redis_client=r, key=\"leaderboard:demo\", max_entries=100)\n\nboard.upsert_user(\n    \"player-1\",\n    1200,\n    {\n        \"name\": \"Ada\",\n        \"description\": \"Solves production incidents before breakfast.\",\n    },\n)\n\nboard.increment_score(\"player-1\", 25)\ntop_players = board.get_top(5)\nplayers_near_rank = board.get_around_rank(rank=10, count=5)",
      "section_id": "the-python-leaderboard"
    },
    {
      "id": "data-model-ex0",
      "language": "text",
      "code": "leaderboard:demo\n  player-1 => 1225\n  player-2 => 1180\n  player-3 => 1105\n\nleaderboard:demo:user:player-1\n  name = Ada\n  description = Solves production incidents before breakfast.",
      "section_id": "data-model"
    },
    {
      "id": "leaderboard-implementation-ex0",
      "language": "python",
      "code": "def upsert_user(\n    self,\n    user_id: str,\n    score: float,\n    metadata: Optional[dict[str, str]] = None,\n) -> dict[str, object]:\n    metadata_key = self._metadata_key(user_id)\n\n    with self.redis.pipeline() as pipeline:\n        pipeline.zadd(self.key, {user_id: float(score)})\n        if metadata:\n            pipeline.hset(\n                metadata_key,\n                mapping={field: str(value) for field, value in metadata.items()},\n            )\n        pipeline.execute()\n\n    trimmed_user_ids = self._trim_to_max_entries()\n    return self.get_user_entry(user_id) or {\n        \"user_id\": user_id,\n        \"score\": float(score),\n        \"metadata\": metadata or {},\n        \"trimmed_user_ids\": trimmed_user_ids,\n    }",
      "section_id": "leaderboard-implementation"
    },
    {
      "id": "leaderboard-implementation-ex1",
      "language": "python",
      "code": "def get_around_rank(self, rank: int, count: int) -> list[dict[str, object]]:\n    normalized_rank = max(1, int(rank))\n    normalized_count = max(1, int(count))\n    half_window = normalized_count // 2\n    start = max(0, normalized_rank - 1 - half_window)\n    end = start + normalized_count - 1\n\n    entries = self.redis.zrange(\n        self.key,\n        start,\n        end,\n        desc=True,\n        withscores=True,\n    )\n    return self._hydrate_entries(entries, start_rank=start + 1)",
      "section_id": "leaderboard-implementation"
    },
    {
      "id": "prerequisites-ex0",
      "language": "bash",
      "code": "pip install redis",
      "section_id": "prerequisites"
    },
    {
      "id": "running-the-demo-ex0",
      "language": "bash",
      "code": "python demo_server.py",
      "section_id": "running-the-demo"
    }
  ]
}
