# Simple Messaging in .NET With Redis Pub/Sub

> Redis is a popular choice for caching data, but its capabilities go far beyond that. One of its lesser-known features is Pub/Sub support. Redis channels offer an interesting approach for implementing real-time messaging in your .NET applications.

Published: 2024-07-27. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/simple-messaging-in-dotnet-with-redis-pubsub

Redis Pub/Sub sends messages to named channels, and every subscriber listening at that moment receives them.
Nothing is stored, so a message published with no subscribers is discarded, which gives you at-most-once delivery.
In .NET you publish and subscribe through an `ISubscriber` from `StackExchange.Redis`.

Redis is a popular choice for [**caching data**](https://milanjovanovic.tech/blog/caching-in-aspnetcore-improving-application-performance), but its capabilities go far beyond that.
One of its lesser-known features is Pub/Sub support.
Redis channels offer an interesting approach for implementing real-time messaging in your .NET applications.
However, as you'll soon see, channels also have some drawbacks.

In this week's newsletter, we'll explore:

- Basics of Redis channels
- Practical use cases for channels
- Implementing a Pub/Sub example in .NET
- Cache invalidation in distributed systems

Let's dive in.

## Redis Channels

Redis channels are named communication channels that implement the [Publish/Subscribe messaging paradigm](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern).
Each channel is identified by a unique name (e.g., `notifications`, `updates`).
Channels facilitate message delivery from publishers to subscribers.

Publishers use the `PUBLISH` command to send messages to a specific channel.
Subscribers use the `SUBSCRIBE` command to register interest in receiving messages from a channel.

![Redis channel with publisher and three subscribers.](https://milanjovanovic.tech/blogs/mnw_100/redis_channel.png)

Redis channels follow a topic-based publish-subscribe model.
Multiple publishers can send messages to a channel, and multiple subscribers can receive messages from that channel.

However, it's crucial to note that Redis channels do not store messages.
If there are no subscribers for a channel when a message is published, that message is immediately discarded.

Redis channels have an **at-most-once delivery** semantics.

## Practical Use Cases

Given that Redis channels operate with **at-most-once delivery** (messages might be lost if there are no subscribers),
they are well-suited for scenarios where occasional message loss is acceptable and real-time or near-real-time communication is desired.

Here are a few possible use cases:

- **Social media feeds**: Broadcasting new posts or updates to users.
- **Live score updates**: Sending live game scores or sports updates to subscribers.
- **Chat applications**: Delivering chat messages in real-time to active participants.
- **Collaborative editing**: Propagating changes in collaborative editing environments.
- **Distributed cache updates**: Invalidating cache entries across multiple servers when data changes. We'll cover this in detail later in the article.

Redis channels aren't the best choice for critical data where message loss is unacceptable.
In such cases, you should consider a [**more reliable messaging system**](https://milanjovanovic.tech/blog/rabbitmq-vs-kafka-dotnet).

Let's see how we can use Redis channels in .NET.

## Pub/Sub With Redis Channels

We will use the `StackExchange.Redis` library to send messages with Redis channels.

Let's start by installing it:

```powershell
Install-Package StackExchange.Redis
```

You can run [Redis](https://redis.io/) locally in a Docker container.
The default port is `6379`.

```
docker run -it -p 6379:6379 redis
```

Here's a simple background service that'll act as our message `Producer`.

We're creating a `ConnectionMultiplexer` by connecting to our Redis instance.
This allows us to obtain an `ISubscriber` that we can use for pub/sub messaging.
The `ISubscriber` will enable us to publish a message to a channel by specifying the channel name.

```csharp {11,19}
public class Producer(ILogger<Producer> logger) : BackgroundService
{
    private static readonly string ConnectionString = "localhost:6379";
    private static readonly ConnectionMultiplexer Connection =
        ConnectionMultiplexer.Connect(ConnectionString);

    private const string Channel = "messages";

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var subscriber = Connection.GetSubscriber();

        while (!stoppingToken.IsCancellationRequested)
        {
            var message = new Message(Guid.NewGuid(), DateTime.UtcNow);

            var json = JsonSerializer.Serialize(message);

            await subscriber.PublishAsync(Channel, json);

            logger.LogInformation(
                "Sending message: {Channel} - {@Message}",
                message);

            await Task.Delay(5000, stoppingToken);
        }
    }
}
```

Let's also introduce a separate background service for consuming messages.

The `Consumer` connects to the same Redis instance and obtains an `ISubscriber`.
The `ISubscriber` exposes a `SubscribeAsync` method that we can use to subscribe to messages from a given channel.
This method accepts a callback delegate that we can use to handle the message.

```csharp
public class Consumer(ILogger<Consumer> logger) : BackgroundService
{
    private static readonly string ConnectionString = "localhost:6379";
    private static readonly ConnectionMultiplexer Connection =
        ConnectionMultiplexer.Connect(ConnectionString);

    private const string Channel = "messages";

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var subscriber = Connection.GetSubscriber();

        await subscriber.SubscribeAsync(Channel, (channel, message) =>
        {
            var message = JsonSerializer.Deserialize<Message>(message);

            logger.LogInformation(
                "Received message: {Channel} - {@Message}",
                channel,
                message);
        });
    }
}
```

Finally, here's what we get when we run both the `Producer` and `Consumer` services:

![Redis channels publish/subscribe demo.](https://milanjovanovic.tech/blogs/mnw_100/redis_pub_sub.gif)

## Cache Invalidation in Distributed Systems

In a recent project, I tackled a common challenge in distributed systems: keeping the caches in sync.
We were using a [**two-level caching**](https://milanjovanovic.tech/blog/fusioncache-multi-level-caching-dotnet) approach.
First, we had an in-memory cache on each web server for super-fast access.
Second, we had a shared Redis cache to avoid hitting our database too often.

The problem was that when data changed in the database, we needed a way to quickly tell all the web servers to clear their in-memory caches.
This is where Redis Pub/Sub came to the rescue.
We set up a Redis channel specifically for cache invalidation messages.

Each application would run a `CacheInvalidationBackgroundService` that subscribes to messages from the cache invalidation channel.

```csharp
public class CacheInvalidationBackgroundService(
    IServiceProvider serviceProvider)
    : BackgroundService
{
    public const string Channel = "cache-invalidation";

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await subscriber.SubscribeAsync(Channel, (channel, key) =>
        {
            var cache = serviceProvider.GetRequiredService<IMemoryCache>();

            cache.Remove(key);

            return Task.CompletedTask;
        });
    }
}
```

Whenever data changes in the database, we publish a message on this channel with the cache key of the updated data.
All the web servers are subscribed to this channel, so they instantly know to remove the old data from their in-memory caches.
Since the in-memory cache is wiped if the application isn't running, losing cache invalidation messages isn't a problem.
This keeps our caches consistent and ensures our users always see the most up-to-date information.

## In Summary

Redis Pub/Sub is not a silver bullet for every messaging need, but its simplicity and speed make it a valuable tool.
Channels allow us to easily implement communication between loosely coupled components.

Redis channels have at-most-once delivery semantics, so they're best suited for cases where the occasional dropped message is acceptable.

I used it to solve the challenge of synchronizing caches across multiple servers.
This allowed our system to serve up-to-date data without sacrificing performance.

**P.S.** When you're ready to dive deeper into creating message-driven systems, check out [**Modular Monolith Architecture**](https://milanjovanovic.tech/modular-monolith-architecture).
I have an entire module dedicated to building reliable distributed messaging and event-driven architecture.

Good luck out there, and see you next week.

---

## Frequently asked questions

### What are Redis channels?

Redis channels are named communication channels that implement the publish/subscribe messaging paradigm. Publishers send messages to a channel with the PUBLISH command, subscribers register interest with SUBSCRIBE, and multiple publishers and subscribers can share the same channel.

### Does Redis Pub/Sub store messages?

No. Redis channels do not store messages. If no subscriber is listening when a message is published, the message is immediately discarded. That gives Redis Pub/Sub at-most-once delivery semantics.

### When should you use Redis Pub/Sub?

Use it when occasional message loss is acceptable and you want real-time or near-real-time communication: social media feeds, live score updates, chat, collaborative editing, or distributed cache invalidation. For critical data where loss is unacceptable, pick a more reliable messaging system.

### How do you implement Redis Pub/Sub in .NET?

Install the StackExchange.Redis package, create a ConnectionMultiplexer for your Redis instance, and get an ISubscriber. The publisher calls PublishAsync with a channel name and payload, and the consumer calls SubscribeAsync with a callback that handles incoming messages.

### How do you invalidate caches across multiple servers with Redis Pub/Sub?

With two-level caching (in-memory per server plus shared Redis), publish the cache key to a dedicated invalidation channel whenever data changes. Every server subscribes and removes that key from its in-memory cache. Lost messages are tolerable because the in-memory cache resets on restart anyway.
