Building Leaderboards with Redis Sorted Sets

A
Author
28 Jun, 2026 1 min read 1.45K views

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.

Leave a comment

Comments are reviewed before they appear.