Redis Cheatsheet

Lists

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

Command table

A Redis list is a doubly linked list of listpack nodes (a quicklist). Pushing and popping at either end is O(1), reaching into the middle is O(n). That single fact decides every correct use of the type: queues and capped logs, never random access.

CommandWhat it doesComplexity
LPUSH key v [v ...]Prepend, return the new lengthO(n) values
RPUSH key v [v ...]Append, return the new lengthO(n) values
LPUSHX / RPUSHX key vPush only if the key already existsO(1)
LPOP key [count]Pop from the head, count is 6.2+O(count)
RPOP key [count]Pop from the tailO(count)
LLEN keyLength, 0 if absentO(1)
LRANGE key start stopInclusive slice, negatives count from the tailO(offset+count)
LINDEX key iElement at an index, nil if out of rangeO(n)
LSET key i vOverwrite an existing index, error if out of rangeO(n)
LINSERT key BEFORE/AFTER pivot vInsert next to the first matching valueO(n)
LREM key count vRemove matching elements, see the sign ruleO(n)
LTRIM key start stopKeep only that range, discard the restO(n removed)
LPOS key v [RANK r] [COUNT n] [MAXLEN m]Index of a value (6.0.6+)O(n)
RPOPLPUSH src dstTail of src to head of dst (superseded by LMOVE)O(1)
LMOVE src dst LEFT/RIGHT LEFT/RIGHTAtomic move, any two ends (6.2+)O(1)
LMPOP n k [k ...] LEFT/RIGHT [COUNT c]Pop from the first non-empty key (7.0+)O(n+c)

There is no LDEL by index. Deleting element 5 means LSET to a sentinel then LREM, which is the usual sign you wanted a sorted set.

Slicing and inspecting

RPUSH hu:log:job:9 a b c d e     # (integer) 5
LRANGE hu:log:job:9 0 -1         # a b c d e, -1 is the last index
LRANGE hu:log:job:9 0 2          # a b c, stop is INCLUSIVE
LRANGE hu:log:job:9 -2 -1        # d e
LINDEX hu:log:job:9 0            # a
LLEN   hu:log:job:9              # 5
LPOP   hu:log:job:9 2            # a b, count form pops several

LRANGE's stop is inclusive, unlike almost every slice API you have used. LRANGE k 0 -1 is the whole list, and out-of-range indices are clamped rather than an error.

LPOS finds a value without pulling the list into your client.

RPUSH q a b a c a > /dev/null
LPOS q a                # 0, the first match
LPOS q a RANK -1        # 4, search from the tail
LPOS q a RANK 2         # 2, the SECOND match
LPOS q a COUNT 0        # 0 2 4, every match, COUNT 0 means all
LPOS q z MAXLEN 100     # nil, and it gave up after 100 comparisons

LREM's count argument is signed, which is easy to get backwards:

countRemoves
> 0That many matches, scanning head to tail
< 0That many matches, scanning tail to head
0Every match

A capped log with LTRIM

LTRIM keeps a range and throws the rest away, which makes a fixed-size ring buffer out of two commands. The pair is the standard "last N events" structure.

LPUSH  hu:events:user:1042 "$(date -u +%FT%TZ) login"
LTRIM  hu:events:user:1042 0 99          # keep the newest 100, drop the rest
LRANGE hu:events:user:1042 0 9           # the 10 most recent

Push to the head and trim 0 N-1 so index 0 is always newest. Run the LTRIM on every push, not periodically, since it is O(number removed) and that is normally 1. Wrap the two in a pipeline or MULTI so a crash cannot leave the list unbounded, and set a TTL on the key so an abandoned log eventually disappears.

Blocking commands

The blocking variants are what make a list a real queue. The consumer sleeps inside Redis instead of polling.

CommandWhat it does
BLPOP key [key ...] timeoutBlock until the head of some key is poppable
BRPOP key [key ...] timeoutSame from the tail
BLMOVE src dst LEFT/RIGHT LEFT/RIGHT timeoutBlocking LMOVE (6.2+)
BRPOPLPUSH src dst timeoutOlder form of BLMOVE src dst RIGHT LEFT
BLMPOP timeout n k [k ...] LEFT/RIGHT [COUNT c]Blocking LMPOP (7.0+)
BLPOP hu:q:high hu:q:low 5     # 1) "hu:q:high"  2) "job-payload"
BLPOP hu:q:high hu:q:low 0     # 0 means block forever

Facts that matter:

- The reply names which key it popped from, so a multi-key BLPOP gives you strict priority. Keys are checked left to right, so hu:q:high always drains first. - timeout is a double since 6.0, so BLPOP q 0.05 is legal. - On timeout the reply is nil (a nil array in RESP2). - Inside MULTI a blocking command does not block. It behaves like the non-blocking version and returns nil immediately. - A blocked connection consumes a connection slot. Size maxclients for your worker count. - Blocked clients are served in FIFO order, so the longest waiting consumer wins.

Simple queue, and why it loses jobs

# Producer
LPUSH hu:q:email '{"to":"ada@example.com","tpl":"welcome"}'

# Consumer
BRPOP hu:q:email 0

LPUSH at the head plus BRPOP at the tail is FIFO and takes two commands total. It is the right answer for work you can afford to lose, and the wrong answer otherwise, because the moment BRPOP returns, the job exists only in the worker's memory. A crash, a deploy, or an OOM kill and it is gone with no trace.

Reliable queue

The fix is to never remove a job without simultaneously recording that someone took it. BLMOVE does both in one atomic step.

# Producer
LPUSH hu:q:email "$job"

# Consumer: atomically claim into a per-worker processing list
job=$(redis-cli BLMOVE hu:q:email "hu:q:email:proc:$WORKER" RIGHT LEFT 0)

# ... do the work ...

# Acknowledge by removing exactly that payload
redis-cli LREM "hu:q:email:proc:$WORKER" 1 "$job"

Recovery is now possible, because a crashed worker leaves its jobs visible in its processing list.

# Requeue a dead worker's in-flight jobs, oldest first
while redis-cli LMOVE "hu:q:email:proc:$DEAD" hu:q:email LEFT RIGHT | grep -q .; do :; done

The remaining caveats are real and worth stating plainly:

- Delivery is at least once. A worker that finishes the job then dies before LREM will have the job replayed, so handlers must be idempotent. - LREM matches by value, so two identical payloads are indistinguishable. Put a unique id in every job. - Nothing detects a stuck worker. You need a separate reaper comparing a per-job heartbeat key against a deadline, and you have to decide how long is too long. - There are no per-job retry counts, no delays, and no dead-letter queue. You build all three or you outgrow the pattern.

When to stop. If you need consumer groups, acknowledgement tracking, pending-entry inspection, or automatic claim of abandoned work, that is exactly what streams provide natively. Reach for XADD plus XREADGROUP rather than reinventing it on lists.

Lists versus the alternatives

NeedUseWhy not a list
Fire-and-forget fanoutPub/SubA list is point to point, one consumer wins
Consumer groups, acks, replayStreamLists have no ack and no history
Priority by numberSorted setZPOPMIN pops the lowest score
Delay until a timestampSorted setScore by unix time, poll with ZRANGEBYSCORE
Random access by indexHash or sorted setLINDEX is O(n)
Deduplicated membershipSetLists happily hold duplicates

Keep an eye on list-max-listpack-size (128 by default). Beyond it each node becomes a separate allocation, and list-compress-depth can LZF-compress interior nodes when you have very long lists that are only touched at the ends.