Scaling SignalR with a Redis Backplane
A single SignalR server works brilliantly until you need a second one. The moment you put a load balancer in front of multiple servers, you hit the fundamental problem: a client connected to Server A won't receive messages sent from Server B. A Redis backplane solves this by acting as a message bus between your servers.
The Problem
SignalR maintains in-memory mappings of connections to groups and users. When you call Clients.All.SendAsync(...), it iterates over the connections known to that server instance. If you have three servers behind a load balancer, each server only knows about roughly a third of your clients.
Without a backplane, Clients.All really means "all clients connected to this server."
How the Redis Backplane Works
With a Redis backplane, every SignalR message is published to Redis, and all servers subscribe to those messages. When Server A sends to a group, it publishes the message to Redis. Servers B and C receive it and forward it to their locally connected clients in that group.
The flow is:
- Hub on Server A calls
Clients.Group("room-1").ReceiveMessage(...). - Server A delivers to its local clients in "room-1" and publishes to Redis.
- Servers B and C receive the Redis message and deliver to their local clients in "room-1".
Setup
Install the Microsoft.AspNetCore.SignalR.StackExchangeRedis NuGet package:
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
Then configure it in Program.cs:
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration = builder.Configuration
.GetConnectionString("Redis")!;
});
That's it. Your hubs, groups, and user targeting all work across servers without any code changes.
Configuration Options
For more control, configure the Redis connection explicitly:
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration =
ConfigurationOptions.Parse("redis-server:6379,password=secret,abortConnect=false");
// Prefix for Redis channels — useful if multiple apps share a Redis instance
options.Configuration.ChannelPrefix =
RedisChannel.Literal("MyApp:");
});
The ChannelPrefix is important when multiple applications share the same Redis instance. Without it, messages from different applications could interfere with each other.
Sticky Sessions
Even with a Redis backplane, you need sticky sessions (session affinity) on your load balancer. SignalR's negotiation step and the subsequent WebSocket connection must hit the same server. The negotiate endpoint returns a connection ID, and the WebSocket handshake must go to the server that created it.
Configure this on your load balancer using a cookie or the connection ID. In Azure App Service, enable ARR Affinity. In NGINX:
upstream signalr_servers {
ip_hash;
server server1:5000;
server server2:5000;
server server3:5000;
}
server {
location /hubs/ {
proxy_pass http://signalr_servers;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
What the Backplane Does NOT Do
It's important to understand the limitations:
- No message persistence. If a server is down when a message is published, those clients miss the message. Redis pub/sub is fire-and-forget.
- No connection state synchronisation. Each server still tracks its own connections. The backplane only forwards messages.
- No guaranteed ordering across servers. Messages sent from different servers may arrive in different orders.
Monitoring
Keep an eye on Redis performance. Every SignalR message adds load to Redis. Monitor:
- Redis memory usage
- Pub/sub channel counts
- Network latency between your servers and Redis
- Message throughput
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration =
ConfigurationOptions.Parse("redis-server:6379");
options.ConnectionFactory = async writer =>
{
var connection = await ConnectionMultiplexer.ConnectAsync(
options.Configuration, writer);
connection.ConnectionFailed += (_, args) =>
{
Console.WriteLine($"Redis connection failed: {args.Exception}");
};
connection.ConnectionRestored += (_, _) =>
{
Console.WriteLine("Redis connection restored.");
};
return connection;
};
});
Performance Considerations
The Redis backplane adds latency — typically 1-2ms per message for the Redis round-trip. For most applications this is negligible, but for high-frequency scenarios (like live gaming), consider:
- Sending fewer, batched messages instead of many small ones.
- Using MessagePack serialisation to reduce payload size.
- Placing Redis in the same data centre as your servers.
- Using Redis Cluster for very high throughput.
When to Consider Azure SignalR Service
If managing Redis and sticky sessions sounds like overhead you don't want, Azure SignalR Service handles all of this for you. It acts as both the backplane and the connection manager, offloading WebSocket management entirely from your servers. We'll cover that in a later article.
Key Takeaways
- A Redis backplane enables SignalR to work across multiple servers by broadcasting messages through Redis pub/sub.
- Setup is a single NuGet package and one line of configuration.
- Sticky sessions are still required on the load balancer.
- The backplane doesn't persist messages or synchronise connection state.
- Monitor Redis performance as message volume grows.