SignalR Redis Backplane ā Scaling Real-Time to Multiple Instances
Scale SignalR across multiple API instances with a Redis backplane: how the backplane works, setup, sticky sessions vs backplane, monitoring backplane health, and the production patterns for high-availability real-time.
The Multi-Instance Problem
SignalR connections are stateful ā each client is connected to one server instance. Without a backplane, a message sent from server A only reaches clients connected to server A.
Without backplane (3 instances):
Instance A: clients 1, 2, 3
Instance B: clients 4, 5, 6
Instance C: clients 7, 8, 9
Doctor on client 1 updates a drug order (Instance A)
Notification sent to ward group
Result: clients 2, 3 receive it (also on Instance A)
Clients 4-9 do NOT receive it (on B and C)
With Redis backplane:
Instance A receives message ā publishes to Redis
Redis broadcasts to Instances B, C
All clients on B and C receive the notification
All 9 clients get the updateSetup
// NuGet: Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR()
.AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!,
options =>
{
options.Configuration.ChannelPrefix =
new RedisChannel("SystemForge", RedisChannel.PatternMode.Auto);
// Prefix namespaces your messages ā avoids conflicts with other apps
});// appsettings.json
{
"ConnectionStrings": {
"Redis": "redis-primary.systemforge.internal:6379,password=secure,ssl=true,abortConnect=false"
}
}Sticky Sessions vs Backplane
Sticky Sessions (affinity):
Load balancer routes the same user to the same instance every time
Pros: simpler (no backplane), no latency overhead
Cons: if that instance crashes ā user loses connection, must reconnect
uneven load distribution (one instance handles all connections for a user)
scale-in removes an instance ā disconnects all its clients
Redis Backplane:
Any instance can receive messages, all instances get them via Redis
Pros: truly stateless API instances ā any instance can serve any client
instance crash only disconnects that instance's clients (others unaffected)
proper horizontal scaling
Cons: Redis adds ~1-2ms per message latency
Redis becomes a single point of failure (mitigated with Redis cluster)
Recommendation: Redis backplane for any production deployment with 2+ instancesRedis Backplane with Resilience
// Configure reconnection and retry for the Redis connection
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration = new ConfigurationOptions
{
EndPoints = { "redis-primary:6379", "redis-replica:6379" },
Password = builder.Configuration["Redis:Password"],
Ssl = true,
AbortOnConnectFail = false,
ConnectRetry = 5,
ReconnectRetryPolicy = new LinearRetry(1000), // 1s between retries
};
});What the Backplane Does NOT Do
The Redis backplane handles:
ā Broadcasting messages across instances
ā Group membership is per-instance ā groups are NOT shared via Redis
Group membership is NOT shared:
Client connects to Instance A ā joins "ward:4B" group on Instance A
Client disconnects and reconnects ā may connect to Instance B
ā "ward:4B" group on Instance B is empty for this client
ā Client must re-join the group
Solution: clients must re-subscribe to groups after (re)connecting// Re-subscribe to groups after reconnect
connection.onreconnected(async () => {
await connection.invoke("SubscribeToWard", currentWardId);
await connection.invoke("SubscribeToAlerts", subscribedAlertTypes);
});Monitoring Backplane Health
// Check Redis connectivity as part of health checks
builder.Services.AddHealthChecks()
.AddRedis(builder.Configuration.GetConnectionString("Redis")!,
name: "redis-backplane",
failureStatus: HealthStatus.Degraded,
tags: ["signalr", "redis"]);
// SignalR-specific health monitoring
app.MapHub<ClinicalDashboardHub>("/hubs/clinical");
app.MapHealthChecks("/health/signalr", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("signalr")
});Azure SignalR Service ā Managed Alternative
// Azure SignalR Service handles scaling without managing Redis yourself
// All connections go through Azure's managed service
builder.Services.AddSignalR()
.AddAzureSignalR(builder.Configuration["AzureSignalR:ConnectionString"]!);
// Advantages:
// ā No Redis to manage
// ā Handles up to 1 million concurrent connections
// ā Built-in high availability
// ā Serverless SignalR (Azure Functions)
// Disadvantages:
// ā Cost (Azure consumption-based pricing)
// ā Vendor lock-in
// ā Latency depends on Azure region proximityLoad Balancer Configuration
# nginx ā required settings for WebSocket support with SignalR
upstream api_servers {
server api1.systemforge.internal:5000;
server api2.systemforge.internal:5000;
server api3.systemforge.internal:5000;
# No sticky sessions needed with Redis backplane
}
server {
location /hubs/ {
proxy_pass http://api_servers;
proxy_http_version 1.1;
# Required for WebSocket upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
# Timeout for long-lived connections
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}Production issue I've seen: A team deployed SignalR behind a load balancer without configuring sticky sessions OR a Redis backplane. Ward alerts reached roughly 33% of nurses (only those on the same instance as the server that fired the alert). Two nurses on different instances saw different ward states. Adding the Redis backplane immediately fixed the consistency issue.
Key Takeaway
The Redis backplane synchronizes SignalR messages across all instances ā any instance can broadcast to any client regardless of which instance they are connected to. Group membership is per-instance; clients must re-join groups after reconnect. Configure the load balancer for WebSocket upgrade headers and remove sticky sessions when using the backplane. For managed scaling, Azure SignalR Service replaces Redis entirely but at higher cost.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.