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:

  1. Hub on Server A calls Clients.Group("room-1").ReceiveMessage(...).
  2. Server A delivers to its local clients in "room-1" and publishes to Redis.
  3. 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:

Example.cs
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis

Then configure it in Program.cs:

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:

Example.cs
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:

Monitoring

Keep an eye on Redis performance. Every SignalR message adds load to Redis. Monitor:

Example.cs
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:

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