jonnifyredis

R4.05 · Time, Delay & Priority

Leaderboards

A sorted set is a leaderboard: store each player's score, and rank, top-N, and around-me queries fall out of the score order — no rank is stored, the rank is computed on read.

A leaderboard answers one need: who is ahead, and by how much. A list cannot express it — a list keeps members in the order they were filled, with no notion of which is highest. A sorted set can. Score each player by their points and the set orders its members by score for free; the rank of any player is their position in that order, the top-N is a read of the first N positions, and an around-me window is a read of the positions either side of a player's own. No position is stored. The rank is recomputed on every read in O(log N), so it is always exactly the score order, never a stale snapshot. This module takes the textbook leaderboard apart and then bridges to the one place Portal would apply it: ranking learners by the progress percent it already records.

Core operations — ZADD and the score

A sorted set stores members with associated scores and maintains order by score automatically. The leaderboard is one sorted set; each member is a player, each score is that player's points.

ZADD leaderboard 1500 "player:alice"
ZADD leaderboard 2300 "player:bob"
ZADD leaderboard 1800 "player:charlie"

ZADD is the whole write: a member and a score. Re-running ZADD on a member that already exists overwrites its score and re-places it in the order. Read a single player's score with ZSCORE, which is O(1).

ZSCORE leaderboard "player:alice"   # the player's score, O(1)

The board · eight players ordered by score, the rank read on the fly

leaderboard — ZREVRANGE 0 -1 (rank · player · score)

The score is the entire ordering: read the set high-to-low and the leader is at the top; a player's rank is their position in that order, never a stored column.

Rank vs reverse rank

A rank is a player's position in the order. Redis offers the position from each end:

  • ZRANK returns the position sorted low-to-high — the lowest score is rank 0.
  • ZREVRANK returns the position sorted high-to-low — the highest score is rank 0.

For a leaderboard where a higher score is better, ZREVRANK is the one to read: the leader sits at rank 0, the runner-up at rank 1. A rank is 0-based, so "rank 0" is first place. The two commands are mirrors of one position in the same order; reading one end never changes the set.

ZREVRANK leaderboard "player:alice"   # 0 = top
ZRANK leaderboard "player:alice"      # 0 = bottom

The rank is a position counted from one end: ZREVRANK from the top (rank 0 is first), ZRANK from the bottom; the two add up to N − 1.

Players around a rank

To show a player their standing against nearby competitors, read a window of positions either side of their own. First find the player's rank, then read the rank window around it:

ZREVRANGE leaderboard 45 55 WITHSCORES   # positions 45–55 from the top

This reads positions 45 through 55 from the top — the players ranked 46th through 56th in 1-based terms. The same two reads serve any "around-me" view: ZREVRANK to locate the player's position, then a rank-range read centred on it. The window is a slice of the existing order; the board is unchanged. The second dive runs both reads over the same roster.

An around-me view is two reads of the order: ZREVRANK to locate the rank, then a rank-range read for the band either side of it.

Tiebreaking with composite scores

When two players share a score, a plain sorted set falls back to comparing the members lexicographically — not the order a ranking should hold. A sorted set has no secondary sort key, so encode both the points and the tiebreaker into a single score. Pack the points into the high digits and the inverted timestamp into the low digits:

composite = points * 10000000000 + (MAX_TIMESTAMP - timestamp)

Higher points always dominate; for equal points, the earlier timestamp ranks higher because it is subtracted from a maximum. The points are recovered by integer division. This is the same score-packing idea the priority queue uses to order equal-priority jobs by arrival — covered in R4.03 · Composite priority scores. One score carries two axes, the high digits dominating the low.

A tie is broken by packing two axes into one score: points in the high digits, the inverted timestamp in the low; the high digits dominate the order, the same packing the priority queue uses.

Pagination and efficient player info

A large board is read a page at a time — each page is a rank range:

ZREVRANGE leaderboard 0 19 WITHSCORES    # page 1: ranks 1–20
ZREVRANGE leaderboard 20 39 WITHSCORES   # page 2: ranks 21–40

ZCARD returns the member count, which gives the page count. To read a player's rank and score together in one network round trip, pipeline the two reads:

ZREVRANK leaderboard "player:alice"
ZSCORE leaderboard "player:alice"

Both run in a single round trip when pipelined, returning the rank and the score together.

A page is a rank range, ZCARD gives the count for the page math, and a pipelined ZREVRANK + ZSCORE returns rank and score in one round trip.

Variations — time-windowed and aggregating

A "this week" or "today" board is a separate sorted set under a time-keyed name, given a TTL so it expires on its own:

ZADD leaderboard:weekly:2024-W05 1500 "player:alice"
EXPIRE leaderboard:daily:2024-01-30 172800   # keep the day's board two days

Several windows combine with ZUNIONSTORE, whose AGGREGATE option chooses how scores merge — SUM for total points across days, MAX for a personal best, MIN for a best time. Redis 6.2 folded the range commands into a unified ZRANGE with a REV option, so ZRANGE … REV reads the same top-N as the older ZREVRANGE; the ordering is identical, only the spelling changed.

A windowed board is one sorted set per time key with a TTL; ZUNIONSTORE merges windows, and Redis 6.2's unified ZRANGE … REV reads the same order under a newer name.

Where this is heading — EchoMQ 2.0

A leaderboard is a generic sorted-set ranking; it is not part of the EchoMQ queue protocol. Where EchoMQ does rank with a sorted set is its priority queue (R4.03's emq:{queue}:prioritized), which orders jobs by a packed score exactly as a leaderboard orders players. The settled EchoMQ 2.0 design — the protocol break on the first EMQ rung, emq.1 — renames that keyspace to emq:{queue}:prioritized, replacing the emq: prefix with emq:, declaring every Lua key in KEYS[], and bumping meta.version to echomq:2.0.0, with the ranking math unchanged. A sorted-set ranking, whether a leaderboard or a priority queue, is a property of the score order alone — independent of the prefix or the version it rides on.

The pattern, applied — Portal's progress as the data a ranking sorts

The pattern lands twice: the sorted-set leaderboard, and the one place Portal would apply it. Portal records a learner's progress through a lesson as a percent — the Portal.Enrollment.Progress struct (%Portal.Enrollment.Progress{id, enrollment_id, lesson_id, percent}, the field percent :: 0..100, the namespace PRG), held as a row in an in-memory, namespace-partitioned store. Portal does not run a Redis sorted set and has no progress ranking; the percent is a plain stored value.

A leaderboard that ranks learners by progress is exactly this pattern applied to that data. Score each learner by their percent — ZADD board <percent> <learner> — and the top-N learners are a ZREVRANGE, a learner's standing is a ZREVRANK, and an around-me view is a rank-range read. The data already exists as a percent; the sorted set is the standard way to add the ranked query over it. State it plainly: Portal stores progress as a percent today, and the sorted-set ranking is the ordinary structure to reach for when a ranked view of that percent is wanted.

the pattern

A sorted set ranks players by score in O(log N), with the rank computed on read and never stored: ZADD the score, ZREVRANGE the top, ZREVRANK a player's place.

in Portal

Portal stores each learner's lesson progress as a percent (the PRG struct, an in-memory store row). Ranking those learners by progress is the leaderboard pattern as the ranking view over that data — ZADD board percent learner, ZREVRANGE for the top, ZREVRANK for "where am I". Portal runs no sorted set; the ranked query is the standard structure to add over the percent it records.

A leaderboard never stores a rank; it stores scores and reads the order. Portal stores a progress percent — the ranked view over it is a sorted set away.

The three dives

Each dive takes one layer of the leaderboard, building on the source spine and bridging to the progress percent a ranking would sort: the score in and the rank out, the two read shapes over one order, and the two ways a score updates. Read them in order — the core first, the reads second, the update third.

A door, not a depth

This module cites a clean standalone leaderboard as the pattern and Portal.Enrollment.Progress as the data a ranking would sort. Where EchoMQ ranks for real — intra-group ordering and priority inside a job group — is the subject of the dedicated EchoMQ course, built next with this toolkit. From here, open onto E4 · Groups. This module teaches the leaderboard; that course teaches the group ranking that uses the same score order.

After R4.05, the Time, Delay & Priority chapter closes with the EchoMQ scheduler workshop. Return to the Time, Delay & Priority chapter to continue.

References

The Redis commands behind a leaderboard — the sorted set as a ranking, the score write, the top-N and rank reads, the in-place accumulate — and where the group ranking that uses the same order is taught.

Sources

Related in this course