How does the cost of a Redis PUBLISH grow as the number of registered pattern subscriptions increases, and why can heavy PSUBSCRIBE usage add latency for clients doing unrelated work on the same instance?
answer
- PUBLISH is O(N+M), M = server-wide patterns
- patterns are a flat list, never indexed
- matched per publish even when nothing matches
- leading * costs more per match
- PUBSUB NUMPAT is the M you can measure
basics
~20 sPUBLISH is roughly O(N+M): N subscribers on the exact channel (a hash lookup plus a write each) and M registered patterns, every one glob-matched against the channel name on every publish. That matching runs on the same single command loop everyone else waits on.
solid answer
~50 sExact channels are cheap: a dictionary lookup gives the subscriber list, so cost scales with the number of actual receivers. Patterns are not indexed. Redis keeps a flat list of every registered pattern and, for **each publish**, glob-matches all of them against the published channel name. So the complexity is O(N+M), where M is the total pattern count across all clients, and that term is paid even when nothing matches. Because command execution is single-threaded in Redis, this matching happens on the same loop that serves every `GET` and `SET`. Thousands of patterns times a high publish rate turns into steady CPU per publish and visible p99 latency for unrelated clients, with `PUBLISH` showing up in `SLOWLOG` and `latency` reports. Mitigations: prefer exact channels with an explicit subscription per name, keep pattern count small and patterns cheap (anchored prefixes, no leading `*`), watch `PUBSUB NUMPAT`, and be careful with keyspace notifications, which can generate a very high publish rate consumed via patterns.
code
text · 12 linesPUBSUB NUMPAT # (integer) 2048 <- the M in O(N+M), server-wide
PUBSUB CHANNELS # exact channels with at least one subscriber
INFO commandstats
# cmdstat_publish:calls=51203344,usec=41207881,usec_per_call=0.80
# ^ compare before/after pattern growth
SLOWLOG GET 5 # PUBLISH entries appear once M is large
# keyspace notifications: a common source of very high publish rates
CONFIG GET notify-keyspace-events # "KEA" = everything; prefer e.g. "Ex"
# consumed as: PSUBSCRIBE __keyevent@0__:expiredgo deeper
Know that PUBLISH costs more when many patterns are registered, because every pattern is checked on each publish.
State the O(N+M) shape, explain that M is checked per publish whether or not it matches, and that exact channels use a dictionary lookup instead.
Tie it to the single execution loop and other clients' p99, name the diagnostics (PUBSUB NUMPAT, commandstats, SLOWLOG), and give mitigations: exact channels, anchored patterns, fewer server-wide patterns, careful keyspace-notification classes.
Frame it as a fanout-architecture decision: pattern taps as a shared-resource tax, channel naming that makes exact subscription viable, application-level redistribution, and when the requirement really calls for a durable stream instead of a live tap.
## The two halves of a publish When `PUBLISH channel payload` runs, Redis does two distinct pieces of work. **Exact-channel delivery.** Subscriptions by literal name live in a dictionary keyed by channel. One hash lookup yields the subscriber list, then Redis appends the message to each subscriber's output buffer. Cost is O(1) lookup plus O(N) writes for N actual subscribers, which is inherent fanout you cannot avoid. **Pattern delivery.** Pattern subscriptions cannot be indexed by name, because the key is a glob rather than a value. Redis therefore keeps them in a flat list and, for every publish, iterates the entire list and runs its glob matcher on `(pattern, published channel)`. Cost is O(M) matcher invocations for M registered patterns **regardless of how many match**, plus a write per match. Documented complexity for `PUBLISH` is O(N+M). ## Why the M term hurts more than it looks Three multipliers make this term dangerous: 1. **It is paid per publish, not per subscriber.** At 50,000 publishes per second with 2,000 registered patterns, that is 100 million glob matches per second of pure overhead before any message is delivered. 2. **It is paid on the single command-execution loop.** Redis executes one command at a time, so this work directly delays every other client's commands. The symptom is not 'Pub/Sub is slow' but 'our cache reads have a bad p99', which sends people hunting in the wrong place. 3. **Pattern cost is not uniform.** A leading `*` forces the matcher to try many positions, so `*error*` is markedly more expensive per call than an anchored `logs.api.*`. Long channel names amplify this. Add duplicate delivery from overlapping patterns and the fanout write volume grows too, which shows up as output-buffer memory and network bytes rather than CPU. ## Diagnosing it - `PUBSUB NUMPAT` gives the number of registered patterns on the node - the M in the formula. A surprisingly large value is the smoking gun. - `SLOWLOG GET` will contain `PUBLISH` entries once M is large or patterns are expensive. - `INFO commandstats` shows `cmdstat_publish` with calls and `usec_per_call`; compare `usec_per_call` before and after pattern growth. - `LATENCY DOCTOR` / `LATENCY HISTORY command` attribute event-loop stalls to command execution generally. - `CLIENT LIST` shows subscriber connections with their `psub` counts, letting you attribute patterns to owners. ## Mitigations, roughly in order of effectiveness 1. **Use exact channels.** If consumers can enumerate what they need, N exact subscriptions beat one pattern: dictionary lookup instead of a linear scan. This is the single biggest win. 2. **Reduce M, not just per-client patterns.** M is server-wide. A hundred service instances each holding five patterns is M=500 on the node. 3. **Anchor patterns.** Prefer a literal prefix (`orders.eu.*`) over a leading wildcard (`*.eu.*`), which both narrows matches and cheapens each matcher call. 4. **Design channel names for exactness.** Hierarchical names exist so consumers can subscribe to precise leaves; if every consumer ends up needing a pattern, the naming scheme is wrong. 5. **Fan out in the application.** One consumer subscribes to a small set of channels and redistributes internally, replacing many server-side patterns with one in-process dispatch table. 6. **Watch keyspace notifications.** `notify-keyspace-events` makes Redis publish on every matching key event, so enabling `KEA` on a busy instance can generate publish rates far above your application traffic, and those channels are almost always consumed with `PSUBSCRIBE __keyevent@0__:*`. Enable only the event classes you need. 7. **Reconsider the transport.** If the requirement is durable, replayable consumption rather than a live tap, Redis Streams with explicit key-based reads avoid the publish-time pattern scan entirely, at the cost of a different consumption model. ## Cluster note Ordinary `PUBLISH` in a Redis Cluster is broadcast to the other nodes so any subscriber anywhere receives it, which means the pattern-matching cost is incurred on those nodes as well and cross-node traffic scales with publish rate. Shard channels (`SPUBLISH`/`SSUBSCRIBE`, Redis 7.0+) confine a publish to one shard and deliberately have **no** pattern form, which is itself a hint about the cost of pattern matching at scale. ## What an interviewer is listening for The O(N+M) shape with M as the server-wide pattern count paid per publish, the connection to Redis's single execution loop and therefore to unrelated clients' latency, concrete diagnostics (`PUBSUB NUMPAT`, `commandstats`, `SLOWLOG`), and mitigation by preferring exact channels and shrinking or anchoring patterns.
- Is one PSUBSCRIBE orders.* cheaper than 500 exact SUBSCRIBE calls for the 500 order channels?Cheaper for the client to set up, but more expensive for the server on the hot path. Exact subscriptions are resolved by a single dictionary lookup no matter how many exist, whereas each registered pattern is glob-matched on every publish to any channel, including channels it will never match. If the channel set is enumerable, prefer exact subscriptions and accept the bookkeeping.
- Your p99 latency for ordinary GET commands degraded after a new monitoring service was deployed. How would you confirm pattern subscriptions are the cause?Check PUBSUB NUMPAT for a jump in registered patterns and CLIENT LIST to attribute them to the new service's connections. Then look at INFO commandstats for a rising usec_per_call on cmdstat_publish and at SLOWLOG for PUBLISH entries, since the matching runs on the same single execution loop that serves GET. Confirm by having the service narrow or drop its patterns and observing the latency recover.
saying these in an interview costs you the question
- 'Patterns are indexed, so matching is O(1)'
- Thinking the pattern cost is only paid when a pattern actually matches
- Assuming Pub/Sub work happens on a separate thread and cannot affect normal commands
- Counting only your own client's patterns rather than the server-wide total
- Enabling notify-keyspace-events KEA on a busy instance without considering the publish rate