The most surprising thing about scaling SignalR across multiple servers isn’t just that it works, it’s how little of your existing SignalR code you actually need to change.
Let’s see this in action. Imagine you have two web servers, web-01 and web-02, both running your SignalR application. A client connects to web-01 and sends a message intended for all clients. Without a backplane, web-02 would never know about this message.
Here’s a simplified look at how your server-side code might look before a backplane:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<ChatHub>("/chat");
});
}
// ChatHub.cs
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Now, let’s introduce Redis as our backplane. The change is remarkably small, confined mostly to Startup.cs:
// Startup.cs (with Redis backplane)
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.ConnectionRedisManager = new StackExchange.Redis.ConnectionMultiplexerPoolManager(
StackExchange.Redis.ConnectionOptions.Parse("localhost:6379")
);
});
}
// ... rest of Configure and ChatHub.cs remain identical ...
That’s it. The ChatHub code doesn’t change. When web-01 sends a message using Clients.All.SendAsync, the SignalR backplane (now Redis) intercepts this. Redis then acts as a message broker, publishing this message to a specific channel. web-02 (and any other server configured with the same Redis backplane) is subscribed to that channel. When web-02 receives the message from Redis, it knows to deliver it to its own connected clients. This creates the illusion of a single, unified hub across all your servers.
The problem this solves is the inherent isolation of individual web server processes. Each server only knows about the clients directly connected to it. When you scale out by adding more web servers behind a load balancer, you create distinct silos. Without a mechanism for these silos to communicate, a message sent to one server is invisible to clients connected to others. The Redis backplane bridges this gap by providing a shared communication channel.
Internally, SignalR with the Redis backplane uses Redis Pub/Sub. When a server wants to broadcast a message, it publishes that message to a Redis channel. All other servers subscribed to that channel receive the message. SignalR on each server then takes this received message and forwards it to the clients connected to that specific server. This is why the Clients.All call works seamlessly across multiple instances – SignalR abstracts away the underlying network and message passing.
The StackExchange.Redis library is the de facto standard for this. The AddStackExchangeRedis extension method is the key. You provide it with the connection string for your Redis instance. The ConnectionRedisManager is where you configure how StackExchange.Redis manages its connections. Using a ConnectionMultiplexerPoolManager is generally a good practice for performance, ensuring efficient reuse of connections to Redis. The ConnectionOptions.Parse method takes a standard Redis connection string, which can include host, port, password, and other settings. For example, a more robust connection string might look like "localhost:6379,password=your_redis_password,connectTimeout=5000".
One detail often overlooked is how SignalR uses specific Redis keys for internal management, beyond just the Pub/Sub channels. It uses Redis keys to store information about connected clients and server instances, allowing it to coordinate and route messages effectively. When a client connects, SignalR registers its presence with Redis. When a message is sent to a specific user or group, SignalR consults these Redis keys to determine which server(s) the target client(s) are connected to, and then publishes the message to the appropriate Pub/Sub channels for those servers to pick up. This distributed state management is what makes the backplane truly work.
The next logical step after getting your SignalR scaled out is to consider how to handle sticky sessions if your load balancer doesn’t support them, or how to implement more granular messaging patterns like sending to specific users or groups.