Redis Pub/Sub (Publish/Subscribe) Created: 14 Jan 2026 Updated: 14 Jan 2026

Dynamic Messaging Pattern-Based Pub/Sub with * and ?

In a standard Pub/Sub model, subscribers listen to specific, named channels. However, as systems scale—especially in microservices or IoT environments—subscribing to every single channel manually becomes inefficient.

Redis solves this with Pattern Matching Subscriptions (PSUBSCRIBE), allowing you to listen to multiple channels simultaneously using glob-style wildcards.

The Architecture: SUBSCRIBE vs. PSUBSCRIBE

While SUBSCRIBE requires an exact string match, PSUBSCRIBE (Pattern Subscribe) allows the subscriber to define a template.

FeatureSUBSCRIBEPSUBSCRIBE
Matching LogicLiteral String (Exact)Glob-style Pattern
WildcardsNoneSupports *, ?, and []
FlexibilityLow (Specific channels only)High (Dynamic channel groups)

1. The Multi-Level Wildcard: *

The asterisk (*) is used to match any number of characters. In messaging, this is most commonly used for hierarchical routing.

  1. Pattern: orders.*
  2. Matches: orders.created, orders.shipped, orders.cancelled
  3. Use Case: A "Log Analytics" service that wants to listen to all events within the "orders" domain without knowing every specific event name.

2. The Fixed-Length Wildcard: ?

The question mark (?) matches exactly one character. This is perfect for slotted systems or region codes where the length is strictly defined.

  1. Pattern: sensor:??:temp
  2. Matches: sensor:01:temp, sensor:XY:temp
  3. Does NOT Match: sensor:1:temp (too short), sensor:100:temp (too long).
  4. Use Case: A monitoring tool that listens to specific hardware racks labeled with 2-digit IDs.

Real-World Implementation in .NET Core

Using the StackExchange.Redis library, we can implement a pattern-based subscriber easily.

The Subscriber (Pattern Listener)

var redis = ConnectionMultiplexer.Connect("localhost");
var sub = redis.GetSubscriber();

// Using '*' to listen to all sub-channels of 'news'
await sub.SubscribeAsync(new RedisChannel("news.*", RedisChannel.PatternMode.Pattern), (channel, message) => {
Console.WriteLine($"[Pattern Match] Received '{message}' from channel: {channel}");
});

// Using '?' to listen to specific room IDs (e.g., room:1, room:A)
await sub.SubscribeAsync(new RedisChannel("room:?", RedisChannel.PatternMode.Pattern), (channel, message) => {
Console.WriteLine($"[Single Match] Room Alert: {message} on {channel}");
});

The Publisher

The publisher does not need to know if the subscriber is using a pattern. It simply publishes to a literal channel.

// Matches 'news.*'
await sub.PublishAsync("news.sport", "Goal scored!");

// Matches 'room:?'
await sub.PublishAsync("room:5", "Lights turned off");

Critical Considerations for Developers

  1. Multiple Deliveries: If a subscriber listens to both news.* and news.sport, and a message is sent to news.sport, the subscriber will receive the message twice.
  2. Performance: While PUBLISH is $O(N)$ where $N$ is the number of subscribers, pattern matching adds overhead because Redis must check the published channel against all active patterns.
  3. Naming Convention: Always use a consistent delimiter (like : or .) to make your patterns predictable (e.g., app:module:action).

Comparison of Patterns

PatternPublished ChannelResultReason
cache:*cache:user:123MATCH* covers all trailing characters.
user:?:infouser:1:infoMATCH? matches the '1'.
user:?:infouser:10:infoFAIL? expects 1 char, but found 2 ('10').
v1.*.logsv1.api.logsMATCH* matches the 'api' segment.
Share this lesson: