Real time features, live dashboards, chat, collaborative editing, notification badges, all reduce to holding persistent connections to every client, and persistent connections are exactly what stateless, load balanced web tiers are bad at: sticky sessions, connection state lost on every deploy, and memory math that turns ugly at ten thousand sockets. Azure SignalR Service and Web PubSub externalize the connections, and the app tier goes back to being stateless.
Two Services, One Decision
SignalR Service is for .NET shops using the SignalR programming model: hubs, typed clients, automatic transport negotiation, and the existing library ecosystem, with the service transparently taking over connection hosting from your app. Web PubSub is the protocol level sibling: plain websockets (plus its subprotocol for pub sub semantics) for any language or framework, when the clients are Python, mobile, or an IoT dashboard with no SignalR client in sight. The decision is that simple, SignalR model in the codebase means SignalR Service, everything else means Web PubSub, and both share the operational model below.
Modes and the Message Flow
Default mode keeps your app server in the loop: clients negotiate through your API (which is where auth happens), connect to the service, and hub method calls flow app to service to clients, the right shape when server logic reacts to client messages continuously. Serverless mode removes the app server from the steady state: clients connect directly, upstream events fire Azure Functions bindings when messages arrive, and the Functions output bindings broadcast, the right shape for notification fan out and dashboards where the server mostly pushes. The pattern powering most real deployments is mundane and excellent: something happens in the backend (an order ships, a metric ticks), an event lands on the Service Bus or Event Grid machinery from earlier posts, a small function picks it up and pushes to the relevant group, and every subscribed browser updates within milliseconds, no polling, no app server sockets.
public class OrdersHub : Hub
{
public async Task JoinOrderGroup(string orderId)
{
// authorize: does this user own the order?
await Groups.AddToGroupAsync(Context.ConnectionId, $"order-{orderId}");
}
}
// elsewhere, on the shipping event:
await hubContext.Clients.Group($"order-{orderId}")
.SendAsync("orderShipped", payload);
The Security and Scale Notes That Matter
Groups are your fan out primitive, per entity groups like the order above, per tenant groups for dashboards, and group membership is authorization, so the join path must check ownership exactly as any API endpoint would, because a client that can join arbitrary groups reads arbitrary tenants’ events. The negotiate endpoint is the front door: it runs on your API behind the Entra auth from this series, issues short lived access tokens scoped to the user, and the service connection string never reaches a client; managed identity connects your servers and functions to the service, keys stay disabled, private endpoints front the service for internal apps, the invariant checklist. Capacity is units, each carrying a thousand concurrent connections and a messages per second envelope: count connections honestly (every open tab is one), autoscale on the Premium tier absorbs the daily curve, and watch the message count metric because a chatty hub broadcasting to big groups multiplies fast, the billing surprise of this service category. Presence, who is online, is a solved pattern with connection events feeding a Redis set from the caching post rather than something to invent. The result, done right, is the feature users describe as “it just updates”, built on connection infrastructure you never patch at 2 AM.
Cheers
Osama
Leave a Reply