Simple Messaging in .NET with Redis Pub/Sub: A Beginner's Guide
Learn Redis Pub/Sub in .NET with StackExchange.Redis using simple words, a real-life analogy, clean async C# code, diagrams, and when to use it safely.
A cricket commentary on the radio
Picture a big cricket match. Inside the stadium, a commentator speaks into a microphone. Their voice goes out over the radio to the whole city.
Now think about who is listening. Some people have their radio switched on and hear every ball. Some people are cooking and only switch on the radio halfway through. A few people forget to turn it on at all. The commentator does not stop and wait for them. They keep talking. Whoever is tuned in right now hears the news. Whoever is not, simply misses it.
The commentator also has no idea how many people are listening. It could be ten people or ten lakh people. They speak once, and the radio carries the words to everyone who is tuned in. There is no list of names. There is no "please reply when you heard me." It is a one-way broadcast.
This is exactly how Redis Pub/Sub works.
- The commentator is your publisher.
- The radio channel is a Redis channel.
- The listeners are your subscribers.
- And if you are not tuned in when the words are spoken, you miss them for good.
That last point is the most important thing to remember in this whole post. Redis Pub/Sub is fast and simple, but it does not save messages for later. Keep the radio picture in your head and the rest will feel easy.
What is Redis, and what is Pub/Sub?
Redis is a very fast in-memory data store. People often use it as a cache to remember things for a short time. But Redis can also pass messages between apps, and that feature is called Pub/Sub, short for Publish/Subscribe.
Here is the idea in three small steps:
- An app subscribes to a channel by name, for example
orders. - Another app publishes a message to that same channel name.
- Redis instantly pushes that message to every app subscribed to
ordersat that moment.
The publisher and the subscriber never talk to each other directly. They only know the channel name. This keeps them loosely coupled, which is a fancy way of saying they do not depend on each other's details. You can add more listeners later without changing the publisher at all.
The one rule you must never forget
Redis Pub/Sub is fire-and-forget. When a message is published, Redis tries to deliver it to whoever is listening at that exact moment, and then forgets all about it.
So if a subscriber is offline, restarting, or simply too slow, it does not get the message and there is no second chance. Redis keeps nothing on disk for Pub/Sub. There is no inbox waiting for you when you reconnect.
This is not a bug. It is the design. It is what makes Pub/Sub so light and fast. But it means you should only use it when missing a message now and then is acceptable.
What happens to a published message
Steps
Publish
App sends to channel
Redis routes
Looks up listeners
Online get it
Delivered instantly
Offline miss it
Gone forever
Setting up the connection
In .NET we talk to Redis using the StackExchange.Redis package. Add it to your project first.
dotnet add package StackExchange.RedisThe heart of this library is the ConnectionMultiplexer. Think of it as one big, shared phone line to Redis. It is expensive to create but safe to share across your whole app and across many threads. So you create it once when the app starts and keep it alive. You should never make a new one for every message.
Here is how you register it in an ASP.NET Core app using dependency injection.
using StackExchange.Redis;
var builder = WebApplication.CreateBuilder(args);
// Create ONE multiplexer for the whole app and share it.
builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
ConnectionMultiplexer.Connect("localhost:6379"));
var app = builder.Build();The word singleton here matters. It tells .NET to build the connection only once and hand out the same one everywhere. From this single connection we can get two useful objects:
- An
ISubscriberfor Pub/Sub messaging. - An
IDatabasefor normal Redis commands like get and set.
The table below shows the main pieces you will use again and again.
| Object | How you get it | What it does |
|---|---|---|
ConnectionMultiplexer | ConnectionMultiplexer.Connect(...) | The shared connection to Redis. Make one, keep it. |
ISubscriber | multiplexer.GetSubscriber() | Publishes and subscribes to channels. |
IDatabase | multiplexer.GetDatabase() | Runs normal commands like StringSet and StringGet. |
RedisChannel | RedisChannel.Literal("orders") | A typed name for the channel you talk on. |
Publishing a message
Publishing is the easy part. You ask the multiplexer for an ISubscriber, then call PublishAsync with a channel name and your message.
Real messages are usually objects, not plain text. So we turn the object into JSON before sending. The subscriber will turn that JSON back into an object on the other side.
using System.Text.Json;
using StackExchange.Redis;
public class OrderPublisher
{
private readonly ISubscriber _subscriber;
public OrderPublisher(IConnectionMultiplexer multiplexer)
{
_subscriber = multiplexer.GetSubscriber();
}
public async Task PublishOrderAsync(Order order)
{
var channel = RedisChannel.Literal("orders");
string json = JsonSerializer.Serialize(order);
// Returns the number of clients that received the message.
long receivers = await _subscriber.PublishAsync(channel, json);
Console.WriteLine($"Order sent to {receivers} listener(s).");
}
}
public record Order(int Id, string Customer, decimal Amount);Notice that PublishAsync returns a number. That number is how many subscribers got the message at that moment. If it comes back as 0, nobody was listening, and your message just vanished. This number is a handy way to check that someone is actually tuned in.
Subscribing to a message
The subscriber side asks for the same channel by name and gives Redis a small piece of code to run whenever a message arrives. That piece of code is called a handler or callback.
using System.Text.Json;
using StackExchange.Redis;
public class OrderSubscriber
{
private readonly ISubscriber _subscriber;
public OrderSubscriber(IConnectionMultiplexer multiplexer)
{
_subscriber = multiplexer.GetSubscriber();
}
public async Task StartListeningAsync()
{
var channel = RedisChannel.Literal("orders");
await _subscriber.SubscribeAsync(channel, (ch, message) =>
{
var order = JsonSerializer.Deserialize<Order>(message!);
Console.WriteLine($"Got order {order!.Id} from {order.Customer}");
});
}
}Once SubscribeAsync runs, your app is tuned in. Every time someone publishes to orders, Redis pushes the message and your handler runs. You do not poll or ask again and again. Redis pushes to you. This is what makes it feel real-time.
Keep your handler fast and safe
There are two small traps that catch beginners. Both are easy to avoid once you know them.
First, do not let your handler throw. If your code inside the handler crashes, it can mess up message processing. Wrap the body in a try/catch so one bad message does not break everything.
Second, do not do slow work inside the handler. Redis delivers messages on a shared path. If your handler takes a long time, like calling a slow database or an external API, you can hold up other messages. The trick is to grab the message quickly and hand the heavy work to a background task or a queue.
await _subscriber.SubscribeAsync(channel, (ch, message) =>
{
try
{
var order = JsonSerializer.Deserialize<Order>(message!);
// Hand off slow work; return from the handler quickly.
_ = Task.Run(() => ProcessOrderAsync(order!));
}
catch (Exception ex)
{
Console.WriteLine($"Bad message skipped: {ex.Message}");
}
});The line _ = Task.Run(...) starts the real work on its own and lets the handler return at once. The underscore just says "I am not waiting for the result here." This keeps the message path quick and smooth.
Channels and pattern channels
So far we used one exact channel name, orders. Redis also lets you subscribe to a pattern so one listener can catch many related channels at once. For example, the pattern orders.* matches orders.created, orders.paid, and orders.cancelled.
You build a pattern channel with RedisChannel.Pattern(...) instead of RedisChannel.Literal(...).
var pattern = RedisChannel.Pattern("orders.*");
await _subscriber.SubscribeAsync(pattern, (ch, message) =>
{
// ch tells you which exact channel fired, e.g. "orders.paid"
Console.WriteLine($"Event on {ch}: {message}");
});This is great when you want one place to watch a whole family of events. The table below compares the two styles.
| Style | Builder | Example match | Good for |
|---|---|---|---|
| Literal | RedisChannel.Literal("orders") | only orders | one clear, named topic |
| Pattern | RedisChannel.Pattern("orders.*") | orders.paid, orders.created | watching a group of related events |
A small warning: pattern matching does a little extra work for Redis, so use exact channels when you can and patterns only when you really need the grouping.
A common real use: cache invalidation
One of the most loved uses of Pub/Sub is telling many servers to clear their local cache at the same time.
Imagine you run three copies of your web app behind a load balancer. Each copy keeps a small in-memory cache of product prices to stay fast. Now an admin changes a price. How do all three copies learn that their cached price is stale?
The answer is a quick Pub/Sub broadcast. When the price changes, you publish a tiny message like product:42 to a cache-invalidate channel. All three servers are subscribed, so all three hear it and drop product 42 from their cache at once. Losing this message is not a disaster, because the cache also expires on its own after a while. That makes it a perfect fit for fire-and-forget.
Cache invalidation broadcast
Steps
Edit price
Data changes in DB
Publish
Send product id
All hear
Every server subscribed
Drop entry
Cache refreshed on next read
Pub/Sub vs Streams vs a full broker
Redis Pub/Sub is not the only choice, and it is important to pick the right tool. Here is the honest comparison.
Redis also has a feature called Streams. Unlike Pub/Sub, a Stream stores messages, supports consumer groups so work can be shared, and lets a consumer pick up where it left off after a restart. It costs a tiny bit more time, often only one or two milliseconds, but it gives you safety and a history.
Beyond Redis, full brokers like RabbitMQ and Azure Service Bus add durable queues, retries, dead-letter handling, and strong delivery promises. They are the right pick when every single message truly must be processed.
| Need | Pub/Sub | Redis Streams | RabbitMQ / Service Bus |
|---|---|---|---|
| Fastest, lightest delivery | Best | Good | Okay |
| Messages saved if no one listens | No | Yes | Yes |
| Replay history after a restart | No | Yes | Yes (durable queues) |
| Share work across many workers | No | Yes (groups) | Yes |
| Best for live, lossy real-time updates | Yes | Sometimes | Overkill |
A simple rule of thumb: if losing a message now and then is fine, reach for Pub/Sub. If every message matters, reach for Streams or a broker.
A note on the .NET ecosystem in 2026
If you came here from a tutorial that used MediatR or MassTransit for messaging, be aware that both moved to a commercial license for newer versions. They are still good tools, but they are no longer free for every company, so check the license before you depend on them in a paid product.
The good news is that StackExchange.Redis stays free and open source, and plain Redis Pub/Sub needs no extra library on top. With .NET 10 now being the LTS release and C# 14 shipped, the simple async code shown above works cleanly and is well supported. You do not need a heavy framework just to broadcast a small message.
Putting it all together
A typical small setup looks like this. One service publishes events as they happen. One or more services subscribe and react. Redis sits in the middle and fans messages out to whoever is tuned in.
Remember the cricket commentary. The orders service is the commentator. Redis is the radio. The dashboard, notify, and cache services are the listeners. Whoever is tuned in hears the news instantly. Whoever steps away misses that one ball. For live updates where speed beats perfect record-keeping, that trade is exactly what you want.
Quick recap
- Redis Pub/Sub lets one app broadcast short messages to every app currently listening on a named channel.
- It is fire-and-forget: if a subscriber is not connected at that moment, the message is lost forever. There is no inbox.
- Use StackExchange.Redis in .NET. Create one
ConnectionMultiplexerat startup and share it everywhere. - Get an
ISubscriberto publish withPublishAsyncand to subscribe withSubscribeAsync. PublishAsyncreturns how many listeners got the message;0means nobody heard it.- Keep your handler fast and wrap it in try/catch so one bad message cannot break the rest.
- Use literal channels for one topic and pattern channels like
orders.*to watch a group of events. - Pub/Sub shines for live, lossy work: dashboards, presence, and cache invalidation.
- When every message must survive a crash, choose Redis Streams or a broker like RabbitMQ or Azure Service Bus.
- Note that MediatR and MassTransit are now commercially licensed; plain Redis Pub/Sub stays free.
References and further reading
- Redis Pub/Sub with StackExchange.Redis — Redis Docs
- Pub/Sub — Redis Glossary
- StackExchange.Redis — GitHub
- Difference between Redis Pub/Sub vs Redis Streams — GeeksforGeeks
- Redis Implementing Pub/Sub and Streams in .NET 10 — DEV Community
Related Patterns
Event-Driven Architecture in .NET with RabbitMQ: A Beginner's Guide
Learn event-driven architecture in .NET with RabbitMQ using simple words, real-life examples, exchanges, queues, and clean async C# code you can copy.
Event-Driven Microservices with Azure Service Bus in .NET
A friendly, step-by-step guide to building event-driven microservices in .NET using Azure Service Bus topics, subscriptions, and the ServiceBusProcessor.
Idempotent Consumer: Handling Duplicate Messages in .NET
Learn the Idempotent Consumer pattern in .NET to safely handle duplicate messages, prevent double charges, and build reliable message-driven systems.
Complete Guide to Amazon SQS and Amazon SNS with MassTransit
A friendly, step-by-step guide to messaging in .NET using Amazon SQS, Amazon SNS, and MassTransit — queues, topics, consumers, retries, and dead-letter handling.
Using MassTransit with RabbitMQ and Azure Service Bus in .NET
Learn how MassTransit lets one set of .NET code run on both RabbitMQ and Azure Service Bus, with simple consumers, publishers, and config examples.
Messaging Made Easy with Azure Service Bus
A simple, friendly guide to Azure Service Bus messaging in .NET — queues, topics, dead-letter queues, sessions, and clean producer and consumer code.