Redis Cheatsheet
Pub/Sub
Use this Redis reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Command table
Pub/Sub is fire and forget. A PUBLISH is delivered to whoever is subscribed at that
instant and then discarded. Nothing is stored, nothing is acknowledged, and there is no
replay.
| Command | What it does | Complexity |
|---|---|---|
PUBLISH channel message | Send, return the number of clients that received it | O(n+m) |
SUBSCRIBE ch [ch ...] | Subscribe to exact channel names | O(n) channels |
UNSUBSCRIBE [ch ...] | Leave channels, all of them with no argument | O(n) |
PSUBSCRIBE pat [pat ...] | Subscribe to glob patterns | O(n) patterns |
PUNSUBSCRIBE [pat ...] | Leave patterns | O(n) |
SSUBSCRIBE ch [ch ...] | Sharded subscribe (7.0+) | O(n) |
SUNSUBSCRIBE [ch ...] | Leave sharded channels | O(n) |
SPUBLISH channel message | Sharded publish (7.0+) | O(n) |
PUBSUB CHANNELS [pattern] | Active channels with at least one subscriber | O(n) |
PUBSUB NUMSUB [ch ...] | Subscriber count per channel | O(n) |
PUBSUB NUMPAT | Total pattern subscriptions across all clients | O(1) |
PUBSUB SHARDCHANNELS [pattern] | Active sharded channels (7.0+) | O(n) |
PUBSUB SHARDNUMSUB [ch ...] | Subscribers per sharded channel | O(n) |
The integer PUBLISH returns is the count of receivers, so PUBLISH returning 0 means
the message went nowhere and there is no error. That is the whole contract, and it is the
one thing to internalize.
PUBLISH hu:notify:1042 '{"type":"mention","post":42}' # (integer) 0, nobody home PUBSUB CHANNELS # (empty array) PUBSUB NUMSUB hu:notify:1042 # 1) "hu:notify:1042" 2) (integer) 0 PUBSUB NUMPAT # (integer) 0
PUBSUB CHANNELS only lists channels with at least one exact subscriber, so a channel
that only pattern subscribers are watching does not appear. Pattern subscriptions show up
in NUMPAT instead.
Patterns
PSUBSCRIBE matches with the same glob syntax as KEYS: * any sequence, ? one
character, [abc] a class, and \ escapes.
PSUBSCRIBE 'hu:notify:*' # every user's channel PSUBSCRIBE 'hu:*:errors' # middle wildcard PSUBSCRIBE 'hu:log:[wi]*' # warn and info
A client subscribed to both hu:notify:1042 and hu:notify:* receives the same message
twice, once as a message frame and once as a pmessage frame. Deduplicate or do not
overlap.
Pattern matching runs for every published message against every registered pattern, so a
thousand patterns multiply the cost of every single PUBLISH. Prefer many exact channels
over broad patterns on a busy server.
The message protocol
A subscribed connection receives push frames rather than replies.
SUBSCRIBE hu:notify:1042 1) "subscribe" 2) "hu:notify:1042" 3) (integer) 1 <- confirmation, 3 is the count 1) "message" 2) "hu:notify:1042" 3) "{...}" <- exact-channel delivery 1) "pmessage" 2) "hu:notify:*" 3) "hu:notify:1042" 4) "{...}" <- pattern delivery
| Frame | Fields |
|---|---|
subscribe / unsubscribe | kind, channel, current subscription count |
psubscribe / punsubscribe | kind, pattern, current count |
ssubscribe / sunsubscribe | kind, shard channel, current count |
message | kind, channel, payload |
pmessage | kind, pattern, channel, payload |
smessage | kind, shard channel, payload |
In RESP2 a subscribed connection is crippled. Once it has any subscription it accepts
only SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, SSUBSCRIBE,
SUNSUBSCRIBE, PING, RESET, and QUIT. Anything else fails:
(error) ERR Can't execute 'get': only (P|S)SUBSCRIBE / (P|S)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context
So a client library needs a second connection for normal commands, which is why every
Redis client has a separate pub/sub connection or client object. RESET (6.2+) clears
the state and returns the connection to normal.
Under RESP3 (HELLO 3) push messages travel on a separate protocol channel, so one
connection can subscribe and run regular commands. That is the modern answer, and it is
what newer clients use by default.
Keyspace notifications
Redis can publish its own events, which is how you observe expiry, eviction, and writes without polling. It is off by default because it costs CPU on every matching command.
CONFIG SET notify-keyspace-events "KEA" # everything CONFIG GET notify-keyspace-events # "AKE", normalized order CONFIG SET notify-keyspace-events "Ex" # expired events only, keyevent channel CONFIG SET notify-keyspace-events "" # off
The value is a flag string, and you must include at least one of K or E or nothing is
published at all.
| Flag | Enables |
|---|---|
K | __keyspace@<db>__:<key> channels, payload is the event name |
E | __keyevent@<db>__:<event> channels, payload is the key name |
g | Generic commands, DEL, EXPIRE, RENAME |
$ | String commands |
l s h z t | List, set, hash, sorted set, stream commands |
d | Module key type events |
x | Expired events |
e | Evicted events (maxmemory pressure) |
n | New key events (7.0+), no key name in the payload |
m | Key-miss events |
A | Alias for g$lshzxetd, everything except m and n |
# Watch every event in db 0 redis-cli PSUBSCRIBE '__key*@0__:*' # React to expiring sessions only redis-cli PSUBSCRIBE '__keyevent@0__:expired'
Real output for SET nk v PX 200 followed by the key expiring, with KEA on:
pmessage __key*@0__:* __keyspace@0__:nk set pmessage __key*@0__:* __keyevent@0__:set nk pmessage __key*@0__:* __keyspace@0__:nk expire pmessage __key*@0__:* __keyevent@0__:expire nk pmessage __key*@0__:* __keyspace@0__:nk expired pmessage __key*@0__:* __keyevent@0__:expired nk
Note the direction: the keyspace channel names the key and the payload is the event, and
the keyevent channel names the event and the payload is the key. Pick whichever way you
want to filter.
Caveats that make this unsuitable as an event source of record:
- Delivery is best effort over pub/sub. A subscriber that is down misses events with no
trace, so never use expired as a job trigger for anything that matters.
- The event carries the key name, never the value. By the time you see expired or
del the value is already gone, so store what you need elsewhere.
- expired fires when the key is actually removed, which is on access or on the sampled
background cycle, so it can arrive noticeably after the TTL elapsed.
- Notifications are per database and the channel name embeds the number, so a SELECT 1
writer publishes to __keyevent@1__:*.
When pub/sub is the wrong tool
| Requirement | Use instead |
|---|---|
| A subscriber may be offline | Stream with a consumer group |
| Acknowledgement or retry | Stream, XACK plus XPENDING |
| Replay history | Stream, XRANGE |
| Exactly one worker per message | List with BLMOVE, or a stream group |
| Durability across a restart | Stream, or an actual broker |
| Backpressure | Anything else, see below |
The failure mode people hit in production is the client output buffer. A slow
subscriber accumulates undelivered messages in server memory, and once
client-output-buffer-limit pubsub is exceeded Redis disconnects it, silently from the
publisher's point of view.
CONFIG GET client-output-buffer-limit # pubsub default: hard 32mb, soft 8mb over 60 seconds CONFIG SET client-output-buffer-limit "pubsub 32mb 8mb 60"
Pub/Sub is genuinely the right answer for cache invalidation fan-out, live presence and typing indicators, config reloads across a fleet, and waking workers that will re-read state from a durable place anyway. In each of those, losing a message means a slightly stale read, not lost work. That is the test to apply.