Blog Backend & APIs
REAL-TIME

Overview of ASP.NET Core SignalR

How SignalR gives an ASP.NET Core app real-time, two-way communication — hubs, transports, and where it fits alongside a regular REST API.

Apr 17, 2023

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.

csharp
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.

javascript
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.

SignalR ASP.NET Core Real-Time

More from the blog

View all articles

Read something that fits your problem?

Tell us what you're building. We'll come back with an honest, scoped recommendation — not a sales pitch.

Start a Project