In a single-server deployment, SignalR works without additional configuration. In a multi-instance deployment (Kubernetes, Azure App Service with multiple instances), each server instance maintains its own set of active WebSocket connections. A message sent from Server A only reaches clients connected to Server A — clients on Servers B and C are missed. The backplane solves this: it is a shared message bus (Redis pub/sub) that all instances publish to and subscribe from, ensuring every server instance can reach every connected client.
Redis Backplane
// dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
// ── Configure Redis backplane ──────────────────────────────────────────────
builder.Services.AddSignalR()
.AddStackExchangeRedis(
builder.Configuration.GetConnectionString("Redis")!,
opts =>
{
opts.Configuration.ChannelPrefix = RedisChannel.Literal("BlogApp:SignalR:");
});
// ── How the backplane works ───────────────────────────────────────────────
// WITHOUT backplane (2 servers):
// Client A connected to Server 1
// Client B connected to Server 2
// Server 1 calls Clients.All.SendAsync("PostPublished", post)
// → Client A receives it ✓
// → Client B does NOT receive it ✗ (Server 1 has no connection to Client B)
// WITH Redis backplane:
// Server 1 publishes "PostPublished" to Redis channel
// Server 2 subscribes to the Redis channel
// Server 2 receives the message and delivers to Client B ✓
// All clients on all servers receive the message
// ── Azure SignalR Service — managed alternative ───────────────────────────
// dotnet add package Microsoft.Azure.SignalR
// builder.Services.AddSignalR().AddAzureSignalR(connectionString);
// Azure manages connection scaling, backplane, and client connections
// App servers handle Hub logic only (not connections) — much simpler at scale
Clients.All.SendAsync(), it publishes the serialised message to a Redis channel. All other server instances are subscribed to that channel and receive the message, then forward it to their local connections. This works for Clients.All, Clients.Group(), and Clients.User() — but adds a Redis network hop for every SignalR message. For very high-throughput scenarios, Azure SignalR Service offloads connection management to a dedicated service.Backplane Message Flow
// ── Message flow with Redis backplane ─────────────────────────────────────
//
// POST /api/posts/42/publish (hits Server 1)
// ↓
// PostService.PublishAsync(42)
// ↓
// hubContext.Clients.All.SendAsync("PostPublished", postDto)
// ↓
// SignalR on Server 1 publishes to Redis pub/sub channel
// ↓
// ┌─────────────────────────┐
// │ Redis Pub/Sub │
// └─────────────────────────┘
// ↓ ↓
// Server 1 Server 2 (Server 3, etc.)
// forwards to receives from
// local clients Redis, forwards
// to local clients
// ── Verify backplane is working ───────────────────────────────────────────
// 1. Run 2 instances of the API (different ports)
// 2. Connect Angular clients to both instances
// 3. Publish a post via instance 1
// 4. Verify both clients receive the "PostPublished" event
Common Mistakes
Mistake 1 — Using sticky sessions in Kubernetes (breaks on pod restart)
❌ Wrong — pod restarts cause all sticky sessions to break; clients reconnect to a different pod and lose group membership.
✅ Correct — use Redis backplane or Azure SignalR Service for proper multi-instance support.
Mistake 2 — Forgetting ChannelPrefix when sharing Redis between apps
❌ Wrong — two apps on the same Redis share SignalR pub/sub channels; App A’s messages reach App B’s clients.
✅ Correct — always set a unique ChannelPrefix per application.