Building Leaderboards with Redis Sorted Sets
Ranking millions of players by score with SQL means sorting large tables. A Redis sorted set keeps members ordered by score at all times, so rank queries take microseconds.
Adding and updating scores
await redis.zAdd('lb:global', [{ score: 1520, value: 'asha' }, { score: 1810, value: 'ravi' }]);
await redis.zIncrBy('lb:global', 50, 'asha'); // asha just earned 50 points
Top 10
const top = await redis.zRangeWithScores('lb:global', 0, 9, { REV: true });
// [{ value: 'ravi', score: 1810 }, { value: 'asha', score: 1570 }, ...]
A player's rank and neighbours
const rank = await redis.zRevRank('lb:global', 'asha'); // 0-based; null if not present
const score = await redis.zScore('lb:global', 'asha');
// Players around asha (two above, two below)
const around = await redis.zRangeWithScores('lb:global', Math.max(0, rank - 2), rank + 2, { REV: true });
Daily and weekly boards
const today = new Date().toISOString().slice(0, 10);
const key = `lb:daily:${today}`;
await redis.zIncrBy(key, points, playerId);
await redis.expire(key, 60 * 60 * 24 * 8); // keep a week of history
// Weekly = union of the last 7 daily boards
await redis.zUnionStore('lb:weekly', last7Days.map((d) => `lb:daily:${d}`));
Breaking ties
Equal scores are ordered by member name, which isn't fair. To rank earlier achievers higher, encode time into the score:
// Higher points first; for equal points, earlier time ranks higher
const MAX_TS = 9_999_999_999;
const score = points * 1e10 + (MAX_TS - Math.floor(Date.now() / 1000));
Keep the combined value below 2^53 so it stays an exact integer in a double.
Other uses for sorted sets
- "Trending" items with time-decayed scores.
- Delayed job queues (score = run-at timestamp).
- Sliding-window rate limiters (score = request time).
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.