Pattern Subscriptions (PSUBSCRIBE)

PSUBSCRIBE lets a client subscribe to every channel whose name matches a glob-style pattern, instead of listing exact channel names one by one. Where SUBSCRIBE ties a client to a fixed set of channels, PSUBSCRIBE ties it to a rule, so new channels that fit the pattern are covered automatically the moment someone publishes to them. This makes pattern subscriptions the natural fit for dynamic, per-entity channels: per-user notifications, per-room chat, per-store order events, where you cannot know every channel name in advance.

Overview: How Pattern Subscriptions Work

Redis Pub/Sub is built around two structures the server keeps in memory: a dictionary mapping each channel name that has at least one exact subscriber to the list of clients subscribed to it, and a list of pattern-and-client pairs for every client that has issued PSUBSCRIBE. These are separate mechanisms. Subscribing with SUBSCRIBE registers you in the first structure under an exact name; subscribing with PSUBSCRIBE registers you in the second, keyed by a pattern rather than a literal string.

When a client runs PUBLISH channel message, Redis does two things, both inside the same single-threaded command execution so no other command can interleave and split the delivery: first it looks up the channel in the exact-match dictionary and pushes the message to every client subscribed there; second it walks the full list of subscribed patterns and runs a glob match — the same matcher used by KEYS and SCAN MATCH — between each pattern and the channel name, pushing the message to every client whose pattern matches. Because the second step is a linear scan over every pattern subscribed anywhere on the server, not just patterns related to this channel, the cost of every single PUBLISH grows with the total number of active pattern subscriptions, even ones that never match anything. This is the main reason pattern subscriptions do not scale as cleanly as exact ones on a server with thousands of concurrent pattern subscribers.

A pattern-matched delivery arrives at the subscriber as a four-element push: pmessage, the pattern that matched, the actual channel name, and the message body. This is different from an exact SUBSCRIBE delivery, which is a three-element message push with no pattern field. If a single client subscribes to a channel both by exact name and by a matching pattern, it receives the message twice — once as message and once as pmessage — because the two delivery paths are independent.

Glob matching in Redis has no concept of hierarchy levels. Unlike topic systems such as MQTT, where a plus sign matches one topic level and a hash matches many, Redis patterns treat the whole channel name as a flat string: an asterisk matches any sequence of characters, including none, and including colons or other separators you use in your own naming convention; a question mark matches exactly one character; and square brackets match a character class. There is no built-in idea of one level down versus any depth.

Like all Pub/Sub in Redis, pattern subscriptions are fire-and-forget: messages are never stored, never replayed, and never delivered to a client that was not connected and subscribed at the moment PUBLISH ran. A pattern subscriber that connects after a message was published simply never sees it, and a subscriber that briefly disconnects loses everything published during the gap. If you need guaranteed delivery, replay, or consumer groups, that is what Redis Streams are for; Pub/Sub, pattern-based or not, intentionally trades durability for simplicity and speed.

There is one more detail worth knowing up front: on the classic RESP2 protocol, once a connection issues SUBSCRIBE or PSUBSCRIBE, that connection is restricted to Pub/Sub and connection-management commands until it unsubscribes from everything. Trying to run an ordinary command like GET on that same connection returns an error. RESP3 clients, connected via HELLO 3, do not have this restriction, since pushed Pub/Sub messages are delivered out-of-band from normal replies, but the RESP2 behavior is still what you will see with a plain redis-cli session, and it is why production code always uses a dedicated connection for subscribing.

Syntax

PSUBSCRIBE pattern [pattern ...]
PUNSUBSCRIBE [pattern [pattern ...]]
  • pattern — a glob-style pattern, the same syntax used by KEYS and SCAN MATCH. You can pass one or several patterns in a single PSUBSCRIBE call; the client is subscribed to all of them.
  • PUNSUBSCRIBE with no arguments unsubscribes the connection from every pattern it is currently subscribed to; with arguments, it unsubscribes only from the listed patterns.
Symbol Meaning Example
* matches any sequence of characters, including zero characters news:* matches news:, news:sports, news:sports:nfl
? matches exactly one character room:? matches room:1 but not room:12
[abc] matches one character from the set log:[eiw]nfo matches channels like log:info
[a-z] matches one character in the range shard:[0-9] matches shard:0 through shard:9
[^abc] matches one character not in the set env:[^t]est excludes env:test
\ escapes a glob character to match it literally price:\*usd matches the literal channel price:*usd
Command Purpose Time Complexity
PSUBSCRIBE subscribe to one or more patterns O(N) for N patterns given in the call
PUNSUBSCRIBE unsubscribe from one or more patterns O(N+M), N patterns removed, M total patterns subscribed server-wide
PUBLISH send a message to a channel O(N+M), N exact subscribers of the channel, M total patterns subscribed server-wide
PUBSUB CHANNELS list channels with at least one exact subscriber O(N), N active channels
PUBSUB NUMSUB count exact subscribers per channel O(N), N channels requested
PUBSUB NUMPAT count unique patterns subscribed server-wide O(1)

Examples

Example 1: A basic pattern subscription

Pattern subscriptions always involve at least two connections: one that subscribes and blocks waiting for pushes, and another that publishes. Open a second redis-cli window and run the subscribe side first:

PSUBSCRIBE news.*

Output:

Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "news.*"
3) (integer) 1

The connection is now blocked, waiting for pushes. In the original window, publish a message on a channel that matches the pattern:

PUBLISH news.sports "Team wins championship"

Output:

(integer) 0

The integer reply is the number of clients the message was delivered to, counted on the publishing connection itself; it is 0 here because that connection has no visibility into a subscriber running on a separate connection. Run the two independently in two real terminals to see the full round trip: the subscribing terminal would show a pushed pmessage array containing "pmessage", "news.*", "news.sports", and the message text the instant the PUBLISH command runs.

Example 2: Inspecting Pub/Sub state with PUBSUB

The PUBSUB command family lets you introspect subscriptions from any ordinary connection, without entering subscribe mode yourself:

PUBSUB CHANNELS
PUBSUB NUMPAT
PUBSUB NUMSUB news.sports

Output:

(empty array)
(integer) 0
1) "news.sports"
2) (integer) 0

With no active subscribers, PUBSUB CHANNELS returns an empty array, PUBSUB NUMPAT reports zero patterns subscribed, and PUBSUB NUMSUB echoes back the channel name paired with a subscriber count of zero. In a live system with an active PSUBSCRIBE connection, NUMPAT would report 1 or more, though NUMSUB still would not count pattern subscribers, only exact ones, since it is scoped strictly to SUBSCRIBE.

Example 3: Realistic use case — per-user notification fan-out

A common pattern is giving each user several delivery-method channels and letting one subscriber catch all of them with a single pattern. A worker process fans out a notification across email and SMS channels:

PUBLISH notify:user:1001:email "Your order has shipped"
PUBLISH notify:user:1001:sms "Your order has shipped"
PUBLISH notify:user:2002:email "Password changed"

Output:

(integer) 0
(integer) 0
(integer) 0

A dashboard for user 1001 only needs to see events addressed to that user, regardless of delivery method, so it subscribes with a pattern scoped to that user’s namespace:

PSUBSCRIBE notify:user:1001:*

Output:

Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "notify:user:1001:*"
3) (integer) 1
1) "pmessage"
2) "notify:user:1001:*"
3) "notify:user:1001:email"
4) "Your order has shipped"
1) "pmessage"
2) "notify:user:1001:*"
3) "notify:user:1001:sms"
4) "Your order has shipped"

Notice the message published to notify:user:2002:email never appears; the pattern is scoped to user 1001, so a different user’s channels stay invisible to this subscriber even though all three channels share the same notify:user: prefix.

How It Works, Step by Step

  1. A client opens a connection and sends PSUBSCRIBE notify:user:1001:*. Redis appends the pattern-and-client pair to its in-memory pattern subscription list and immediately replies with a confirmation array: the string psubscribe, the pattern, and the client’s total subscription count.
  2. A second, independent client sends PUBLISH notify:user:1001:email "Your order has shipped".
  3. Still inside that single PUBLISH command’s atomic execution, Redis first checks its exact-channel dictionary for notify:user:1001:email. If no client used plain SUBSCRIBE on that exact name, this step finds nothing.
  4. Redis then scans its pattern list and tests each pattern against the channel name with the same matcher KEYS uses. notify:user:1001:* matches notify:user:1001:email, so the message is queued for delivery to that client.
  5. The subscriber’s socket receives a pushed four-element array: pmessage, the pattern that matched, the real channel name, and the message body. redis-cli prints this as soon as it arrives, without the subscriber having sent any new command.
  6. PUBLISH itself returns to the publisher an integer: the total number of clients — exact plus pattern-matched — that the message was delivered to.

Common Mistakes

Mistake: Treating patterns as hierarchical topics

Coming from a system like MQTT, it is tempting to assume you need something like orders:*:created to catch one level and a different pattern for deeper nesting. Redis has no concept of levels; an asterisk matches any run of characters, including colons, so a single orders:* already matches orders:store42:created, orders:store42:region9:created, and everything else that starts with orders:. Writing an overly specific pattern like orders:*:created can silently miss channels that do not have exactly that shape.

PSUBSCRIBE orders:*

Output:

Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "orders:*"
3) (integer) 1
1) "pmessage"
2) "orders:*"
3) "orders:store42:created"
4) "order #9981 placed"

The fix is to trust the flat, single-wildcard pattern rather than hand-rolling one wildcard per expected level; test any pattern against real channel names before relying on it in production.

Mistake: Running normal commands on a subscribed connection

Once a connection has issued PSUBSCRIBE, RESP2 restricts it to Pub/Sub and connection commands. Trying to slip in an ordinary read fails:

PSUBSCRIBE session:*
GET user:1001:name

Output:

Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "session:*"
3) (integer) 1
(error) ERR Can't execute 'get': only (P|S)SUBSCRIBE / (P|S)UNSUBSCRIBE / PING / QUIT / RESET are allowed in this context

The fix is architectural: open a dedicated connection purely for subscribing and keep it separate from the connection or pool your application uses for regular commands, or switch that connection to RESP3 with HELLO 3, which delivers subscription pushes out-of-band and leaves the connection free for normal commands.

Mistake: Expecting messages published before you subscribed

Pub/Sub, pattern or exact, never buffers or replays. If your subscriber process starts a second after a burst of PUBLISH calls, those messages are gone permanently; there is no error, nothing to indicate loss, the subscriber simply never receives them. This bites teams that use Pub/Sub as an ad-hoc job queue or event log. If losing messages during a deploy or restart is unacceptable, use a Stream — XADD paired with XREADGROUP — or a List — LPUSH paired with BRPOP — instead, both of which persist the data until a consumer reads it.

Best Practices

  • Use a dedicated connection for PSUBSCRIBE or SUBSCRIBE; never share it with a connection your application also uses for regular reads and writes.
  • Keep patterns as narrow as the use case allows; every active pattern, matching or not, adds to the cost of every PUBLISH on the server.
  • Design channel names with a predictable, colon-namespaced convention such as entity:id:event, so patterns stay simple and intention-revealing.
  • Never use Pub/Sub, pattern-based or exact, where you need delivery guarantees, message history, or replay; reach for Streams instead.
  • Watch PUBSUB NUMPAT and PUBSUB NUMSUB in production to catch subscriber leaks — connections that subscribed and never unsubscribed.
  • Avoid catch-all patterns like a bare asterisk in production; they receive every message published anywhere on the server and add matching overhead to every single PUBLISH.
  • If you need to keep issuing normal commands over the same connection you subscribe on, use RESP3 with HELLO 3 so subscription pushes do not lock the connection into Pub/Sub-only mode.

Practice Exercises

1. Match the pattern. Given the pattern chat:room42:*, decide which of these channel names would and would not match, then verify by starting two redis-cli sessions and testing with PSUBSCRIBE in one and PUBLISH in the other: chat:room42:messages, chat:room42, chat:room420:messages, chat:room42:messages:system.

2. Build a namespaced notification pattern. Design channel names for a food-delivery app that needs to notify a single courier about new orders, order cancellations, and route changes. Write out the channel naming convention, then write a single PSUBSCRIBE pattern that catches all three event types for one courier but not for any other courier.

3. Confirm cleanup. Subscribe to two patterns on one connection, check PUBSUB NUMPAT to confirm the server sees two, then run PUNSUBSCRIBE with one specific pattern argument and check PUBSUB NUMPAT again. Expected end state: the count drops by exactly one, and the connection is still subscribed to the remaining pattern.

Summary

  • PSUBSCRIBE pattern [pattern ...] subscribes a connection to every channel matching one or more glob patterns, rather than fixed names.
  • Patterns use the same glob syntax as KEYS and SCAN MATCH: asterisk, question mark, character classes, negated classes, and backslash to escape.
  • Redis has no hierarchy concept; an asterisk matches any depth of your own naming convention, unlike MQTT-style plus and hash wildcards.
  • Pattern-matched messages arrive as a pmessage push containing the pattern, channel, and message; exact subscriptions arrive as message with just channel and message, so a client subscribed both ways gets both.
  • PUBLISH cost scales with total subscribed patterns server-wide, not just patterns relevant to that channel, so unused broad patterns are not free.
  • Pub/Sub, pattern-based or exact, is fire-and-forget: no storage, no replay, no delivery to disconnected or not-yet-subscribed clients; use Streams when you need durability.
  • PUBSUB CHANNELS, PUBSUB NUMSUB, and PUBSUB NUMPAT let you introspect what is currently subscribed without guessing.