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.
| Flag | Behavior | Use Case |
| None | Default. Waits for Redis to respond with the subscriber count. | Critical messages where you need to verify listeners exist. |
| FireAndForget | Sends the message and continues immediately without waiting for a response. | High-volume telemetry or logs where speed is the priority. |
| HighPriority | Puts 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
- 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.
- Thread Safety: The
ConnectionMultiplexer handles thread safety internally, making it safe to use as a Singleton across your entire application. - Channel Scaling: While you can have thousands of channels, remember that each subscription maintains a small amount of overhead on the Redis server.