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

Redis Pub/Sub in .NET: A Complete Implementation Guide

In distributed systems, services often need to communicate in real-time without being tightly coupled. Redis Pub/Sub is a high-speed messaging pattern where "Publishers" send messages to "Channels" without knowing who—if anyone—is listening. "Subscribers" listen to those channels and react to messages as they arrive.

1. The Foundation: Connecting to Standalone Redis

To begin, we need a robust connection service. In .NET, the ConnectionMultiplexer is designed to be shared and reused. We wrap this in a RedisService to manage the connection to our standalone host.

Configuration (appsettings.json)

{
"RedisOption": {
"Host": "localhost",
"Port": "6379",
"Password": "admin"
}
}

The Connection Manager (RedisService.cs)

public class RedisService
{
private readonly ConnectionMultiplexer? _connectionMultiplexer;

public RedisService(string host, string port, string password, ILogger<RedisService> logger)
{
var connectionString = $"{host}:{port},password={password},abortConnect=false";
_connectionMultiplexer = ConnectionMultiplexer.Connect(connectionString);

if (_connectionMultiplexer.IsConnected)
logger.LogInformation("Connected to Redis at {Host}:{Port}", host, port);
}

public ISubscriber GetSubscriber() => _connectionMultiplexer!.GetSubscriber();
public IDatabase GetDb(int dbIndex = -1) => _connectionMultiplexer!.GetDatabase(dbIndex);
}

2. The Publisher: Sending Messages

The Publisher's job is simple: push data onto a named channel. A key feature of StackExchange.Redis is that the PublishAsync method returns the number of subscribers that received the message.

Publisher Implementation

public class SimplePubSubPublisher(RedisService redisService, ILogger<SimplePubSubPublisher> logger)
{
private readonly ISubscriber _subscriber = redisService.GetSubscriber();

public async Task PublishMessageAsync(string channel, string message)
{
// We use Literal mode for exact channel matching
var subscriberCount = await _subscriber.PublishAsync(
new RedisChannel(channel, RedisChannel.PatternMode.Literal),
message,
CommandFlags.None // Waiting for response to see subscriber count
);

logger.LogInformation("Message sent to '{Channel}'. Reached {Count} subscribers.", channel, subscriberCount);
}
}

3. The Consumer: Listening in the Background

A Consumer must stay active as long as the application is running. The best way to achieve this in .NET is via a BackgroundService.

Subscriber Implementation

public class SimplePubSubConsumer : BackgroundService
{
private readonly ISubscriber _subscriber;
private readonly ILogger<SimplePubSubConsumer> _logger;

public SimplePubSubConsumer(RedisService redisService, ILogger<SimplePubSubConsumer> logger)
{
_subscriber = redisService.GetSubscriber();
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
string channelName = "notifications";

await _subscriber.SubscribeAsync(new RedisChannel(channelName, RedisChannel.PatternMode.Literal), (channel, message) =>
{
// This logic executes every time a message is received
_logger.LogInformation("Received: {Message} from {Channel}", message, channel);
});

// Keep the service alive
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000, stoppingToken);
}
}
}

4. Fine-Tuning with CommandFlags

When publishing messages, you can control the behavior using CommandFlags. This is crucial for balancing performance and reliability.

FlagBehaviorUse Case
NoneDefault. Waits for Redis to respond with the subscriber count.Critical messages where you need to verify listeners exist.
FireAndForgetSends the message and continues immediately without waiting for a response.High-volume telemetry or logs where speed is the priority.
HighPriorityPuts the message at the front of the internal .NET send queue.Urgent system alerts or circuit-breaker triggers.

5. Wiring Everything Together

Finally, we register these components in Program.cs. Note that the RedisService and Publisher are singletons, while the Consumer is registered as a Hosted Service.

var builder = WebApplication.CreateBuilder(args);

// 1. Connection Service
builder.Services.AddSingleton<RedisService>(sp => {
var config = builder.Configuration.GetSection("RedisOption");
return new RedisService(config["Host"]!, config["Port"]!, config["Password"]!, sp.GetRequiredService<ILogger<RedisService>>());
});

// 2. The Publisher
builder.Services.AddSingleton<SimplePubSubPublisher>();

// 3. The Background Consumer
builder.Services.AddHostedService<SimplePubSubConsumer>();

var app = builder.Build();

// Example Endpoint to trigger a publish
app.MapGet("api/publish", async (SimplePubSubPublisher publisher) => {
await publisher.PublishMessageAsync("notifications", $"Test message at {DateTime.Now}");
return Results.Ok();
});

app.Run();

Important Considerations

  1. At-Most-Once Delivery: Redis Pub/Sub is a "fire and forget" protocol. If a subscriber is offline when a message is sent, they will never receive it. If you need guaranteed delivery, consider using Redis Streams.
  2. Thread Safety: The ConnectionMultiplexer handles thread safety internally, making it safe to use as a Singleton across your entire application.
  3. Channel Scaling: While you can have thousands of channels, remember that each subscription maintains a small amount of overhead on the Redis server.
Share this lesson: