DEV Community

Preeti Islur
Preeti Islur

Posted on

Real-Time Architecture Patterns for Blazor Server and SignalR at Scale

Beyond the basics

Getting a single SignalR Hub broadcasting to a handful of connected clients is the easy part. The real architectural decisions show up once you need real-time features to work reliably across multiple users, multiple servers, and long-running connections that occasionally drop. This is where a lot of Blazor Server / SignalR apps hit a wall that "add a hub method" doesn't solve.

Groups: the first scaling decision

Broadcasting to every connected client is rarely what you want. SignalR Groups let you scope messages to a subset of connections — a specific document's collaborators, a specific tenant's users, a specific dashboard's viewers.

csharp
public class CollaborationHub : Hub
{
public async Task JoinDocument(string documentId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"doc-{documentId}");
}

public async Task NotifyDocumentUpdate(string documentId, string change)
{
    await Clients.Group($"doc-{documentId}").SendAsync("DocumentUpdated", change);
}
Enter fullscreen mode Exit fullscreen mode

}

The common mistake here is forgetting to remove connections from groups on disconnect — OnDisconnectedAsync should clean up group membership, or you'll leak stale group entries that accumulate over the app's lifetime.

Scaling across multiple servers: the backplane problem

A single server holding all SignalR connections in memory works fine until you need more than one server instance — at which point, a client connected to Server A has no way to receive a message triggered by an event on Server B, unless you introduce a backplane.

The standard solution is a Redis backplane, which lets all server instances share connection and group state:

csharp
services.AddSignalR()
.AddStackExchangeRedis(redisConnectionString, options =>
{
options.Configuration.ChannelPrefix = "MyApp";
});

This is the point where "it works on my machine" and "it works behind a load balancer" diverge — if you skip the backplane, users on different server instances simply won't see each other's real-time updates, and this failure mode is easy to miss in local development where everything runs on one instance.

Sticky sessions matter for Blazor Server specifically

Blazor Server has an additional constraint beyond typical SignalR apps: because a circuit holds server-side component state, a user's requests must consistently route to the same server instance for the life of that circuit. If a load balancer round-robins a user's reconnection attempt to a different server, that circuit — and all its in-memory state — is gone.

This means your load balancer needs sticky sessions (session affinity) configured specifically for Blazor Server apps, in addition to any backplane you set up for a separate custom Hub. These solve different problems: the backplane lets separate hub connections communicate across servers; sticky sessions keep a single circuit's requests on one server so its state isn't lost mid-session.

Handling reconnection gracefully

Network drops happen — WiFi hiccups, laptop sleep, mobile network handoffs. Blazor Server has built-in reconnection UI, but the default experience is minimal. A more robust pattern tracks connection state explicitly and gives users clear feedback:

csharp
protected override async Task OnInitializedAsync()
{
hubConnection.Reconnecting += (error) =>
{
connectionState = "Reconnecting...";
InvokeAsync(StateHasChanged);
return Task.CompletedTask;
};

hubConnection.Reconnected += (connectionId) =>
{
    connectionState = "Connected";
    InvokeAsync(StateHasChanged);
    return Task.CompletedTask;
};

hubConnection.Closed += async (error) =>
{
    connectionState = "Disconnected";
    await InvokeAsync(StateHasChanged);
    // Optionally attempt manual reconnect with backoff here
};
Enter fullscreen mode Exit fullscreen mode

}

For custom Hub connections (as opposed to the circuit's own reconnection, which Blazor Server handles separately), building in exponential backoff for reconnect attempts avoids hammering the server during a wider outage.

Message ordering and delivery guarantees

SignalR doesn't guarantee message delivery or ordering out of the box — if a connection drops between two SendAsync calls, the client may simply never receive one of them, with no automatic retry. For features where this matters (financial updates, sequential state changes), consider:

Including a sequence number or timestamp in each message so the client can detect gaps
Falling back to a periodic full-state refresh rather than relying purely on incremental real-time messages
Treating SignalR as a "notify and re-fetch" signal rather than the sole source of truth for critical state
Takeaway

The patterns that matter as real-time apps grow are rarely about the Hub method signatures themselves — they're about what happens when you have more than one server, more than a few users, and connections that don't behave perfectly. Groups, a backplane, sticky sessions for Blazor Server circuits specifically, and honest handling of delivery guarantees are the differences between a demo and something that holds up in production.

Top comments (0)