Sorted Sets
A sorted set stores unique string members ordered by a floating-point score. Insert is O(log N); range queries by score or rank are fast. The right primitive for leaderboards, rate limits, time-windowed queues.
ZADD, ZRANGE, leaderboards, time windows
EXAMPLE
# 1) Add + read
ZADD scores 100 ada 88 bo 92 cy
ZSCORE scores ada # 100
ZRANK scores ada # 2 (ascending — bo, cy, ada)
ZREVRANK scores ada # 0 (descending — ada is top)
# 2) Top-N (descending)
ZREVRANGE scores 0 9 WITHSCORES # top 10 with scores
ZRANGE scores 0 9 WITHSCORES # bottom 10
# 3) Range by score
ZRANGEBYSCORE scores 90 100 # members with 90 <= score <= 100
ZRANGEBYSCORE scores 90 +inf # at least 90
ZCOUNT scores 90 100 # how many in that range
# 4) Increment a score
ZINCRBY scores 5 ada # ada is now 105
# 5) Remove
ZREM scores bo
ZREMRANGEBYRANK scores 0 -100 # remove all but top 100
ZREMRANGEBYSCORE scores -inf 0 # remove negative scores
# 6) Cardinality
ZCARD scores # total count
# 7) Pop highest / lowest
ZPOPMAX scores # 1 highest member + score
ZPOPMIN scores 5 # 5 lowest
BZPOPMIN scores 30 # blocking pop with 30s timeout
# === Real recipes ===
# 8) Leaderboard with score + tie-break
# Trick: encode the timestamp in the score (lower is better)
# composite = score * 1e10 - timestamp_ms
# Higher-score-first; among ties, EARLIEST timestamp wins.
ZADD leaderboard $((1000 * 10**10 - 1717000000)) ada
ZADD leaderboard $(( 950 * 10**10 - 1717000050)) bo
ZREVRANGE leaderboard 0 9 WITHSCORES
# 9) Sliding-window rate limit — 100 requests per minute per user
# - Score = timestamp
# - Trim everything older than 60s
# - Count what remains; reject if >= 100
MULTI
ZADD rate:user:42 $NOW $NOW # member must be unique — use a UUID/op id
ZREMRANGEBYSCORE rate:user:42 -inf $NOW_60
ZCARD rate:user:42
EXPIRE rate:user:42 60
EXEC
# Reject if the ZCARD result > 100
# 10) Time-windowed queue — earliest deadline first
ZADD jobs $DUE_TS job:abc
ZADD jobs $DUE_TS job:xyz
# Worker pops the earliest-due job (atomic via Lua or BZPOPMIN)
BZPOPMIN jobs 30
# 11) Top-K with bounded memory — trim after each insert
ZADD top 99 'event-1' 87 'event-2' ...
# Then trim:
ZREMRANGEBYRANK top 0 -1001 # keep top 1000
# 12) Range by lex — alphabetical (when scores are all 0)
ZADD names 0 ada 0 bo 0 cy 0 di 0 ed
ZRANGEBYLEX names "[b" "[d" # → bo, cy, di
ZRANGEBYLEX names - + # all
# 13) Node — ioredis
import Redis from 'ioredis';
const redis = new Redis();
await redis.zadd('leaderboard', 100, 'ada', 88, 'bo', 92, 'cy');
const top = await redis.zrange('leaderboard', 0, 9, 'WITHSCORES', 'REV');
// [['ada', '100'], ['cy', '92'], ['bo', '88']]
// Increment after a game
await redis.zincrby('leaderboard', 5, 'ada');
// Rate limit (Lua for atomicity)
const now = Date.now();
const windowStart = now - 60_000;
await redis.multi()
.zadd(`rate:${userId}`, now, randomUUID())
.zremrangebyscore(`rate:${userId}`, '-inf', windowStart)
.zcard(`rate:${userId}`)
.expire(`rate:${userId}`, 60)
.exec();
# 14) Performance
# • ZADD / ZRANGE are O(log N)
# • Use ZRANGEBYSCORE LIMIT for paginated leaderboards
# • For huge leaderboards, shard by region and merge top-K with a heap
Why it matters
Sorted sets cover leaderboards, time-windowed rate limits, and earliest-deadline queues with one primitive. The composite-score trick (score * factor - timestamp) gives stable tie-breaks in a single ZADD.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
ZADD leaderboard 1000 "ada" 900 "bo" 1200 "cy" ZREVRANGE leaderboard 0 9 WITHSCORES ZINCRBY leaderboard 50 "ada" ZRANK leaderboard "bo"Try it Yourself »
Exercise
Add a scored member.
board 1000 "ada"
Four letters.
Discussion
Loading…