A lot of "real-time" features get bolted onto a REST API with a client polling every few seconds, which works right up until you need sub-second updates or the poll interval starts hammering the server. SignalR is ASP.NET Core's answer to that — a library that gives the server the ability to push data to connected clients the moment something happens, instead of waiting for the client to ask.
Hubs and the Transport Fallback Chain
The core abstraction is the Hub — a class the server and client both talk through, where methods can be called in either direction. Under the hood, SignalR does not force you to pick a transport; it negotiates one automatically, trying WebSockets first, falling back to Server-Sent Events, and finally to Long Polling if neither is available. That fallback chain is what makes SignalR practical to ship without worrying about corporate proxies or older infrastructure that blocks WebSocket upgrades — the app still works, just with slightly higher latency on the fallback transports.
A Simple Hub
Here is a minimal notification hub. Clients call SendNotification, and the server broadcasts it back out to every connected client through Clients.All.
using Microsoft.AspNetCore.SignalR;
public class NotificationHub : Hub
{
public async Task SendNotification(string user, string message)
{
await Clients.All.SendAsync("ReceiveNotification", user, message);
}
public override async Task OnConnectedAsync()
{
await Clients.Caller.SendAsync("ReceiveNotification", "System", "Connected");
await base.OnConnectedAsync();
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<NotificationHub>("/hubs/notifications");
app.Run();
Clients.All broadcasts to everyone; Clients.Caller replies only to whoever invoked the method; Clients.Group targets a subset — useful for something like a per-dashboard or per-tenant channel where you do not want every connected user getting every update.
Connecting from JavaScript
The client side uses the @microsoft/signalr package. You build a connection against the hub URL, register handlers for the messages the server sends, then start the connection.
import * as signalR from "@microsoft/signalr";
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/notifications")
.withAutomaticReconnect()
.build();
connection.on("ReceiveNotification", (user, message) => {
console.log(`${user}: ${message}`);
});
async function start() {
try {
await connection.start();
console.log("SignalR connected");
} catch (err) {
console.error(err);
setTimeout(start, 5000);
}
}
start();
document.getElementById("sendBtn").addEventListener("click", () => {
connection.invoke("SendNotification", "Dispatcher", "Build finished")
.catch(err => console.error(err));
});
withAutomaticReconnect() is worth turning on for anything beyond a demo — without it, a dropped WebSocket connection (a laptop going to sleep, a flaky network) just ends the connection silently and your UI stops receiving updates with no obvious signal that anything is wrong.
When to Reach for It
- Live dashboards where numbers should update the instant underlying data changes, not on a fixed refresh interval.
- Chat and messaging features, where both low latency and two-way communication matter.
- Progress notifications for long-running background jobs — push "25% done" instead of the client polling a status endpoint.
- Collaborative features like multiple users editing or viewing the same record simultaneously.
- Plain REST is still the right call for standard CRUD operations, anything cacheable, or requests that do not benefit from an open persistent connection.
If you scale out across multiple server instances, in-memory SignalR breaks — a client connected to instance A never hears about a message broadcast from instance B. Wire up a backplane (Azure SignalR Service, or the Redis backplane via Microsoft.AspNetCore.SignalR.StackExchangeRedis) before you go multi-instance, not after something mysteriously stops updating for half your users.
Wrapping Up
SignalR is not a replacement for your REST API — it is a companion for the slice of functionality that genuinely needs the server to speak first. Reach for it deliberately, keep hubs thin, and remember the backplane the moment you plan to run more than one instance.