Redis Cheatsheet

Streams

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

Command table

A stream is an append-only log of entries, each with a monotonic ID and a flat field-value map. Unlike pub/sub it persists, and unlike a list it supports consumer groups with per-message acknowledgement. This is Redis's Kafka-shaped type.

CommandWhat it doesComplexity
XADD key [NOMKSTREAM] [trim] id f v [f v ...]Append an entry, return its IDO(1)
XLEN keyNumber of entries currently storedO(1)
XRANGE key start end [COUNT n]Entries in an ID range, oldest firstO(log n + count)
XREVRANGE key end start [COUNT n]Same, newest first, note the swapped argsO(log n + count)
XREAD [COUNT n] [BLOCK ms] STREAMS k [k ...] id [id ...]Read forward from an IDO(log n + count)
XDEL key id [id ...]Delete entries by IDO(1) per ID
XTRIM key MAXLEN|MINID [~|=] n [LIMIT c]Trim the logO(n removed)
XAUTOCLAIM / XCLAIMReassign pending entries to another consumerO(1) per entry
XSETID key id [ENTRIESADDED n] [MAXDELETEDID id]Force the last-generated IDO(1)
XINFO STREAM|GROUPS|CONSUMERSIntrospectionO(1)

Entry IDs

An ID is <milliseconds>-<sequence>, both 64-bit unsigned, and it strictly increases. * asks the server to generate one, which is what you want in production.

XADD hu:events '*' user 1042 action login
# "1789531888747-0"

XADD hu:events 5-1 a 1        # explicit ID, legal
XADD hu:events 5-2 a 2        # legal, sequence bumped
XADD hu:events 4-1 a 3
# (error) ERR The ID specified in XADD is equal or smaller
#         than the target stream top item

XADD hu:events NOMKSTREAM '*' a 1   # (nil) if the stream does not exist yet
XADD hu:events '5-*' a 1            # 7.0+, server picks the sequence
Special IDWhereMeans
*XADDGenerate from the server clock
-XRANGEThe smallest possible ID
+XRANGEThe largest possible ID
$XREADOnly entries added after this call
>XREADGROUPEntries never delivered to any consumer in the group
0XREADGROUPThis consumer's own pending entries, for recovery
(idXRANGE, XREADExclusive, so start just after id (6.2+)

Because IDs come from the clock, a backwards clock jump makes XADD fail rather than produce an out-of-order log.

Reading without a group

XLEN hu:events
XRANGE hu:events - + COUNT 10          # oldest 10
XREVRANGE hu:events + - COUNT 10       # newest 10, args reversed
XRANGE hu:events "(5-1" +              # everything strictly after 5-1
XRANGE hu:events 1789531000000 1789532000000   # a time window, ms prefix is enough

XREAD COUNT 100 STREAMS hu:events 0                 # from the beginning
XREAD BLOCK 0 STREAMS hu:events '$'                 # tail -f, block forever
XREAD BLOCK 5000 COUNT 10 STREAMS hu:a hu:b '$' '$' # several streams at once

Pagination is a loop of XRANGE with an exclusive start, carrying the last ID you saw:

last="-"
while :; do
  # after the first page, use ( to exclude the ID you already handled
  redis-cli XRANGE hu:events "$last" + COUNT 100
  # set last="($id_of_last_entry)" and break when a page comes back short
done

XREAD with $ is a plain fan-out tail. Every reader independently gets everything, there is no acknowledgement, and nothing tracks position for you. That is fine for a live feed and wrong for work distribution.

Trimming

Streams grow forever unless you trim them. Trimming is an argument to XADD, so it costs nothing extra on the hot path.

XADD hu:events MAXLEN '~' 10000 '*' a 1   # keep ROUGHLY the newest 10000
XADD hu:events MAXLEN  =  10000 '*' a 1   # keep EXACTLY 10000
XADD hu:events MINID  '~' "$(( ($(date +%s) - 86400) * 1000 ))" '*' a 1

XTRIM hu:events MAXLEN '~' 10000
XTRIM hu:events MINID  '~' 1789531000000
XTRIM hu:events MAXLEN '~' 10000 LIMIT 500   # cap the work per call, 6.2+

Always use ~. The tilde means "at least this many, trimmed on macro-node boundaries", so Redis drops whole radix-tree nodes instead of walking entries. = is exact and O(n removed), which is a latency spike waiting to happen on a large trim. MINID trims by age rather than count and is the better fit when entry rate varies.

XDEL removes an entry by ID but does not reclaim its space immediately, and it does not remove the entry from any consumer group's pending list. XLEN counts live entries while XINFO STREAM's entries-added counts everything ever appended.

Consumer groups

A consumer group gives you the thing lists and pub/sub cannot: each entry goes to exactly one consumer in the group, and it stays in that consumer's Pending Entries List (PEL) until acknowledged.

CommandWhat it does
XGROUP CREATE key group id [MKSTREAM] [ENTRIESREAD n]Create a group starting at id
XGROUP CREATECONSUMER key group consumerPre-create a consumer
XGROUP DELCONSUMER key group consumerDrop a consumer, returns its pending count
XGROUP SETID key group idRewind or fast-forward the group
XGROUP DESTROY key groupDelete the group and its PEL
XREADGROUP GROUP g c [COUNT n] [BLOCK ms] [NOACK] STREAMS key idRead as a consumer
XACK key group id [id ...]Acknowledge, removing entries from the PEL
XPENDING key group [IDLE ms] [start end count [consumer]]Inspect the PEL
XCLAIM key group consumer min-idle-time id [...]Take specific entries
XAUTOCLAIM key group consumer min-idle-time start [COUNT n] [JUSTID]Scan and take (6.2+)
# Create the group and the stream in one step. 0 replays history, $ starts at the tail.
XGROUP CREATE hu:events workers 0 MKSTREAM

# Each worker uses a stable, unique consumer name
XREADGROUP GROUP workers worker-1 COUNT 10 BLOCK 5000 STREAMS hu:events '>'

# ... process ...
XACK hu:events workers 1789531888747-0

> means "entries no one has been given yet". Any other ID, and 0 in particular, means "re-read my own unacknowledged entries", which is exactly what a restarting worker does first.

# Worker startup: drain my own PEL before taking new work
XREADGROUP GROUP workers worker-1 COUNT 100 STREAMS hu:events 0
XREADGROUP GROUP workers worker-1 COUNT 10 BLOCK 5000 STREAMS hu:events '>'

NOACK skips the PEL entirely, turning the group into at-most-once delivery. It is faster and it loses messages on a crash.

XGROUP CREATE on a missing stream errors with NOGROUP unless you pass MKSTREAM.

Pending entries and recovery

# Summary form: count, min ID, max ID, then per-consumer counts
XPENDING hu:events workers
# 1) (integer) 2
# 2) "1789531888747-0"
# 3) "1789531888751-0"
# 4) 1) 1) "worker-1"
#       2) "2"

# Extended form: one row per entry, with idle time and delivery count
XPENDING hu:events workers - + 10
# 1) 1) "1789531888747-0"
#    2) "worker-1"
#    3) (integer) 74123      <- ms since it was last delivered
#    4) (integer) 1          <- delivery attempts

XPENDING hu:events workers IDLE 60000 - + 10           # only stale ones
XPENDING hu:events workers - + 10 worker-1             # one consumer

The delivery count is your retry counter. Anything above a threshold is a poison message, so route it to a dead-letter stream and XACK the original.

XAUTOCLAIM is the reaper. It scans the PEL from a cursor and reassigns anything idle longer than the threshold.

# Take over everything idle for more than 60s, in pages
XAUTOCLAIM hu:events workers worker-2 60000 0 COUNT 100
# 1) "0-0"                      <- next cursor, 0-0 means done
# 2) 1) 1) "1789531888751-0"
#       2) 1) "user" 2) "1042"
# 3) (empty array)              <- IDs deleted from the stream, dropped from the PEL

XAUTOCLAIM hu:events workers worker-2 60000 0 COUNT 100 JUSTID   # IDs only
XCLAIM hu:events workers worker-2 0 1789531888751-0              # one specific entry

XCLAIM takes explicit IDs and supports IDLE, TIME, RETRYCOUNT, FORCE (claim an entry not currently in the PEL), and JUSTID (which does not bump the delivery count).

Deleting a consumer hands its work back for someone else to claim:

XGROUP DELCONSUMER hu:events workers worker-1   # returns its pending count

Introspection

XINFO STREAM hu:events          # length, entries-added, last-generated-id, first/last entry
XINFO STREAM hu:events FULL     # includes every group's full PEL, expensive
XINFO GROUPS hu:events          # per group: consumers, pending, last-delivered-id, lag
XINFO CONSUMERS hu:events workers   # per consumer: pending, idle, inactive

lag in XINFO GROUPS is the number of entries the group has not read yet, which is your backlog metric. It reports nil when the stream has been trimmed or XDEL'd in a way that makes the count unknowable, so alert on pending and consumer idle as well.

Stream, list, or pub/sub

Pub/SubList queueStream + group
PersistenceNone, fire and forgetUntil poppedFull log, trimmed on your terms
Offline subscriberLoses everythingFine, work waitsFine, reads its backlog
DeliveryTo every subscriberTo one popperOne consumer per group, per entry
AcknowledgementNoNo, LREM by value at bestNative, XACK plus a PEL
Retry after a crashImpossibleManual processing listXPENDING plus XAUTOCLAIM
Multiple independent readersYes, all get allNo, they competeYes, one group each
Replay historyNoNoYes, XRANGE or XGROUP SETID
Delivery count per messageNoNoYes
CostCheapestCheapMemory for retained entries

The decision is short. Live notifications where a missed message does not matter: pub/sub. Simple work distribution you are willing to hand-roll recovery for: a list with BLMOVE. Anything that needs acknowledgement, retries, replay, or several independent consumer groups over the same events: streams. Delivery is still at least once, so handlers must be idempotent, and Redis is still not a durable message broker, so an unreplicated node loss loses whatever was not persisted.