Scaling SignalR — Redis Backplane for Multi-Server Deployments

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
Note: The Redis backplane for SignalR uses Redis pub/sub channels. When one server instance calls 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.
Tip: For production Kubernetes deployments with SignalR, consider using Azure SignalR Service or AWS Managed Service instead of the Redis backplane. These services handle connection persistence, scaling, and message routing — your app servers contain only Hub logic (no persistent WebSocket connections). The connection count scales independently of your app instances. For self-hosted environments, the Redis backplane is the right choice — it is mature, well-tested, and has minimal overhead for typical message volumes.
Warning: Sticky sessions (load balancer session affinity) route each client to the same server instance for every request. This avoids the need for a backplane but has serious limitations: sticky sessions cause uneven load distribution (some servers get most connections), fail on server restart (client must reconnect to a different server), and do not work with Kubernetes rolling deployments. Sticky sessions are a workaround, not a solution — use the Redis backplane or Azure SignalR Service for proper horizontal scaling.

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.

🧠 Test Yourself

A 3-instance Kubernetes deployment uses the Redis backplane. Server 2 calls Clients.Group("admins").SendAsync("Alert", msg). How many clients receive it?