Redis Cheatsheet

Sorted Sets

Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Command table

A sorted set holds unique members each carrying a double-precision score, kept ordered by score and, for ties, lexicographically by member. It is a skip list plus a hash, which buys O(log n) insertion with O(1) score lookup. This is the most useful type in Redis.

CommandWhat it doesComplexity
ZADD key [opts] score m [score m ...]Add or update membersO(log n) per member
ZINCRBY key delta mAdd to a score, creating the member at 0 firstO(log n)
ZSCORE key mOne score as a string, nil if absentO(1)
ZMSCORE key m [m ...]Several scores (6.2+)O(n) members
ZCARD keyNumber of membersO(1)
ZCOUNT key min maxMembers in a score rangeO(log n)
ZLEXCOUNT key min maxMembers in a lex range, all scores equalO(log n)
ZRANK key m [WITHSCORE]0-based rank low to high, WITHSCORE is 7.2+O(log n)
ZREVRANK key m [WITHSCORE]Rank high to lowO(log n)
ZRANGE key start stop [opts]The one range command, see belowO(log n + count)
ZRANGESTORE dst src start stop [opts]Range straight into a key (6.2+)O(log n + count)
ZREM key m [m ...]Remove membersO(log n) per member
ZREMRANGEBYRANK key start stopRemove by rank positionO(log n + count)
ZREMRANGEBYSCORE key min maxRemove by score rangeO(log n + count)
ZREMRANGEBYLEX key min maxRemove by lex rangeO(log n + count)
ZPOPMIN key [count]Pop the lowest-scoring members (5.0+)O(log n × count)
ZPOPMAX key [count]Pop the highest-scoring membersO(log n × count)
BZPOPMIN / BZPOPMAX key [key ...] timeoutBlocking pop, reply names the keyO(log n)
ZMPOP / BZMPOPMulti-key pop with COUNT (7.0+)O(log n × count)
ZRANDMEMBER key [n [WITHSCORES]]Random members, negative n allows repeats (6.2+)O(n)
ZSCAN key cursor [MATCH p] [COUNT n]Cursor iterateO(1) per call
ZUNIONSTORE dst n k [k ...] [WEIGHTS] [AGGREGATE]Union into a keyO(n log n)
ZINTERSTORE dst n k [k ...] [WEIGHTS] [AGGREGATE]Intersection into a keyO(n log n)
ZDIFFSTORE dst n k [k ...]Difference into a key (6.2+)O(total)
ZUNION / ZINTER / ZDIFF n k [k ...]Same, returning the result (6.2+)O(n log n)
ZINTERCARD n k [k ...] [LIMIT l]Intersection size only (7.0+)O(n×m)

Scores are IEEE 754 doubles, so integers stay exact only up to 2^53. A unix millisecond timestamp (about 1.7×10^12) is well inside that, but a nanosecond one is not.

ZADD options

ZADD is a full upsert, and its flag block is what makes it safe under concurrency.

FlagEffect
NXOnly add new members, never update an existing score
XXOnly update existing members, never add
GTUpdate only if the new score is greater (6.2+), adds new members
LTUpdate only if the new score is less (6.2+), adds new members
CHReturn the count of changed members, not just added ones
INCRTreat the score as an increment, return the new score, one member only
ZADD hu:lb:2026-09 GT CH 10 ada    # 1, added
ZADD hu:lb:2026-09 GT CH  5 ada    # 0, refused, 5 is not greater
ZSCORE hu:lb:2026-09 ada           # "10"
ZADD hu:lb:2026-09 INCR 5 ada      # "15", INCR returns the new score
ZADD hu:lb:2026-09 NX INCR 5 ada   # (nil), NX blocked it, so no score comes back

NX and XX are mutually exclusive, GT/LT/NX are mutually exclusive, and INCR accepts exactly one score-member pair. GT is the correct way to record a high score without a read-modify-write race, and ZADD key INCR is identical to ZINCRBY except that it can carry NX/XX.

Without CH, the integer reply counts only newly added members, so a pure update returns 0 and that is not a failure.

ZRANGE, the modern form

Since 6.2 one command covers every range query. Learn it and you can ignore the five deprecated ones.

ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
ModifierMeaning
(none)start and stop are ranks, 0-based, negatives count from the end
BYSCOREThey are scores, -inf/+inf allowed, ( prefix means exclusive
BYLEXThey are members, [ inclusive, ( exclusive, - and + are the extremes
REVReverse the ordering, which also swaps the argument order
LIMIT offset countPagination, only legal with BYSCORE or BYLEX, count -1 means all
WITHSCORESInterleave scores into the reply
ZADD z 1 a 2 b 3 c 4 d > /dev/null

ZRANGE z 0 -1                              # a b c d, by rank
ZRANGE z 0 -1 REV WITHSCORES                # d 4 c 3 b 2 a 1
ZRANGE z 0 2                                # a b c, stop is INCLUSIVE

ZRANGE z "(1" "+inf" BYSCORE LIMIT 0 2      # b c, score > 1, first two
ZRANGE z "-inf" "2" BYSCORE                 # a b, score ≤ 2
ZRANGE z "+inf" "-inf" BYSCORE REV LIMIT 0 2  # d c, note max comes FIRST

ZRANGE z "[a" "(c" BYLEX                    # a b, member ≥ "a" and < "c"
ZRANGE z "-" "+" BYLEX                      # every member
ZRANGE z "[user:" "[user:\xff" BYLEX        # prefix search, the idiom

With REV the arguments swap. ZRANGE z +inf -inf BYSCORE REV is correct and ZRANGE z -inf +inf BYSCORE REV returns nothing, silently. That is the single most common mistake with this command.

BYLEX is only meaningful when every score is identical, because it walks the skip list assuming member order is the sort order. With mixed scores the result is undefined rather than an error.

Deprecated older forms, all still supported and all replaceable:

DeprecatedModern equivalent
ZREVRANGE k 0 -1ZRANGE k 0 -1 REV
ZRANGEBYSCORE k min maxZRANGE k min max BYSCORE
ZREVRANGEBYSCORE k max minZRANGE k max min BYSCORE REV
ZRANGEBYLEX k min maxZRANGE k min max BYLEX
ZREVRANGEBYLEX k max minZRANGE k max min BYLEX REV

They are marked deprecated as of 6.2 and are not going away, so existing code is fine. Write new code against ZRANGE.

ZRANGESTORE writes a range into another key instead of returning it, which is how you snapshot a page or trim a leaderboard server side.

ZRANGESTORE hu:lb:top100 hu:lb:2026-09 0 99 REV
EXPIRE hu:lb:top100 300

Removing and popping

ZREM hu:lb:2026-09 ada                              # by member
ZREMRANGEBYRANK hu:lb:2026-09 0 -1001               # keep only the top 1000
ZREMRANGEBYSCORE hu:jobs:due "-inf" "$(date +%s)"   # drop everything already due
ZREMRANGEBYLEX  hu:index:names "[a" "(b"            # drop a lex prefix

ZPOPMIN hu:jobs:due 1        # member and score, lowest first
ZPOPMAX hu:lb:2026-09        # the current leader, removed
BZPOPMIN hu:q:p1 hu:q:p2 5   # blocks, reply names the key it popped from
ZMPOP 2 hu:q:p1 hu:q:p2 MIN COUNT 10   # 7.0+, batch form

ZREMRANGEBYRANK key 0 -1001 is the leaderboard-trim idiom: negative ranks count from the top, so this deletes everything except the highest-scoring 1000.

Aggregating across sets

ZUNIONSTORE and ZINTERSTORE take an explicit numkeys count, then optional per-key weights and a combining function.

OptionValues
WEIGHTS w [w ...]One multiplier per input key, default 1
AGGREGATE SUM | MIN | MAXHow to combine scores for a shared member, default SUM
# A 7-day leaderboard from 7 daily ones, all weighted equally
ZUNIONSTORE hu:lb:7d 7 hu:lb:d1 hu:lb:d2 hu:lb:d3 hu:lb:d4 \
                       hu:lb:d5 hu:lb:d6 hu:lb:d7
EXPIRE hu:lb:7d 3600

# Recency-weighted: today counts triple
ZUNIONSTORE hu:lb:w 3 hu:lb:d1 hu:lb:d2 hu:lb:d3 WEIGHTS 3 2 1

# Best single day per player, not the sum
ZUNIONSTORE hu:lb:best 3 hu:lb:d1 hu:lb:d2 hu:lb:d3 AGGREGATE MAX

# Sets count as score 1 per member, so you can mix types
ZINTERSTORE hu:scored:active 2 hu:lb:2026-09 hu:active:today AGGREGATE MIN

ZINTERCARD 2 hu:lb:2026-09 hu:active:today LIMIT 100   # size only, 7.0+
ZDIFF 2 hu:lb:2026-09 hu:banned WITHSCORES             # 6.2+, returns the result

A plain set may be used anywhere these commands take a key, and each of its members contributes score 1 (multiplied by its weight), which is how you filter a leaderboard by an unscored membership set. In Cluster mode every key named must hash to the same slot, so use a hash tag such as hu:{lb}:d1.

Leaderboard

ZINCRBY hu:lb:2026-09 1 "user:1042"                 # award a point

ZRANGE hu:lb:2026-09 0 9 REV WITHSCORES             # top 10
ZREVRANK hu:lb:2026-09 "user:1042" WITHSCORE        # rank and score, 7.2+
ZCARD hu:lb:2026-09                                 # total players
ZCOUNT hu:lb:2026-09 100 "+inf"                     # how many above 100

Rank is 0-based, so display rank + 1. Ties break lexicographically by member, which means alphabetical luck decides order among equal scores. The standard fix is a composite score that encodes time as a tiebreak, for example points * 10^10 + (2^31 - epoch), kept inside the 2^53 exact-integer budget.

A neighbours window around one player:

rank=$(redis-cli ZREVRANK hu:lb:2026-09 "user:1042")
lo=$(( rank > 2 ? rank - 2 : 0 ))
redis-cli ZRANGE hu:lb:2026-09 "$lo" "$(( lo + 4 ))" REV WITHSCORES

Delayed jobs by timestamp

Score by the unix time the job becomes due and the sorted set is a scheduler.

# Schedule for 60 seconds from now
ZADD hu:sched:due "$(( $(date +%s) + 60 ))" "job:9f3a"

# Poller: claim everything already due, atomically
ZRANGE hu:sched:due "-inf" "$(date +%s)" BYSCORE LIMIT 0 100

The read-then-remove version races two pollers against each other. Do it in one script.

-- EVAL <script> 1 hu:sched:due <now> <limit>
local due = redis.call('ZRANGE', KEYS[1], '-inf', ARGV[1],
                       'BYSCORE', 'LIMIT', 0, tonumber(ARGV[2]))
if #due > 0 then
  redis.call('ZREM', KEYS[1], unpack(due))
end
return due

unpack on a very large table overflows the Lua stack, so keep the limit in the hundreds and loop. To make the claim retryable rather than destructive, ZADD each job into an in-flight set with a deadline score instead of ZREM, and reap anything still there later.

Priority queue, same structure, score is the priority:

ZADD hu:q:pri 1 "urgent-job"  5 "normal-job"  9 "batch-job"
ZPOPMIN hu:q:pri              # "urgent-job", lowest score wins

HyperLogLog

Not a sorted set, but it lives next door: a fixed-size probabilistic counter of distinct elements. 12KB per key regardless of cardinality, with a standard error of 0.81 percent.

CommandWhat it doesComplexity
PFADD key el [el ...]Add elements, 1 if the estimate changedO(1) per element
PFCOUNT key [key ...]Estimated cardinality, unioning several keys on the flyO(1), O(n) multi-key
PFMERGE dst src [src ...]Union several HLLs into oneO(n) keys
PFADD hu:uv:2026-09-15 "user:1042" "user:1043"     # 1
PFCOUNT hu:uv:2026-09-15                           # 2
PFCOUNT hu:uv:2026-09-15 hu:uv:2026-09-14          # union, WITHOUT storing it
PFMERGE hu:uv:w37 hu:uv:2026-09-09 hu:uv:2026-09-10
PFCOUNT hu:uv:w37

The trade is stark. A set of 10 million ids costs hundreds of megabytes and gives an exact answer. A HyperLogLog costs 12KB and is within about 0.81 percent. Use it for unique-visitor and unique-event counts where a fraction of a percent does not matter.

Limits: there is no PFREM, you cannot list members, you cannot intersect (only union), and small cardinalities use a sparse encoding that is smaller than 12KB but is converted permanently once hll-sparse-max-bytes is crossed. When you need exact counts or the member list, use a set.