Signalr

By the end of this lesson you'll understand how SignalR lets a server push updates to clients the instant they happen — powering live chat, dashboards, and notifications without the client constantly asking. You'll read real hub code, then drill the underlying publish/subscribe idea in plain C# you can run right here.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of SignalR as a radio station . Once a listener tunes in (connects), the station can broadcast to everyone at once — that's Clients.All . The station doesn't wait for listeners to phone in and ask "any news yet?" every few seconds; it simply pushes the moment something happens. Groups are like private channels on the same station — a sports channel and a music channel — where only the people tuned to that channel hear the broadcast. And just as a radio falls back from FM to AM where the signal is weak, SignalR automatically falls back from WebSockets to other transports so the connection keeps working.

Plain HTTP is request/response : the client asks, the server answers, the connection closes. To show live data that way, the client has to poll — ask "anything new?" over and over on a timer. Polling is wasteful (most answers are "no") and laggy (you only see updates as fast as you ask).

SignalR keeps a single connection open so the server can push a message the instant something changes — no repeated asking. That's the whole reason it exists, and it's the right tool whenever many clients need the same live updates: chat, notifications, live scores, collaborative editing, trading dashboards.

SignalR runs on an ASP.NET Core server, so the hub examples below won't execute in the in-page fiddle. Read them carefully, then practise the same publish/subscribe pattern in the runnable plain-C# exercises — the mental model transfers directly.

The first argument to SendAsync("Name", ...) is the method name the client is listening for — it's a plain string, so a typo silently drops the message. (Strongly-typed hubs replace these strings with interface methods.)

1. The Hub — Where Clients Connect

A Hub is the server-side class clients connect to. Every public method on it is something a client can invoke over the wire, and from inside the hub you push back out with Clients.<target>.SendAsync("HandlerName", args...) . The string "HandlerName" must match a handler the client registered, or the message just vanishes. Read this worked example — it shows the radio broadcast ( Clients.All ) and a private reply ( Clients.Caller ).

SignalR needs a web host, so it won't run in the fiddle — but its broadcasting is really just publish/subscribe : a list of listeners, and a loop that hands each one the message. You can build exactly that in plain C# and run it. Fill in the two ___ blanks:

2. Groups — Private Channels

Groups are named sets of connections — the private channels on the radio station. You add the current connection with Groups.AddToGroupAsync(Context.ConnectionId, room) and broadcast to just that channel with Clients.Group(room).SendAsync(...) . A connection can be in many groups at once, and groups are created and destroyed automatically as connections join and leave. This is how chat rooms, document sessions, and per-topic feeds are built.

Now model groups yourself: a Dictionary mapping each group name to its list of subscribers, so a send reaches only that group. Fill in the two ___ blanks, then run it and confirm the music fan hears nothing:

3. The JavaScript Client

A hub is only half the story — something has to connect to it. The browser uses the @microsoft/signalr package: build a connection to the hub URL, register connection.on("HandlerName", ...) handlers, then start() . Register your handlers before start() so you don't miss messages that arrive during connection, and use withAutomaticReconnect() so a dropped link silently reconnects.

4. The Connection Lifecycle

SignalR creates a new hub instance for every call and discards it, so you must never store per-user state in hub fields — it won't survive. Instead, hook the lifecycle: override OnConnectedAsync when a client joins and OnDisconnectedAsync when it leaves (whether it closed cleanly, crashed, or timed out). Each connection gets a fresh Context.ConnectionId that is valid only for that one connection — it changes on every reconnect, so never persist it as a user identifier.

SignalR is a layer over several transports — the actual technique used to keep data flowing. When a client connects, SignalR negotiates the best one both sides support and silently falls back if it can't be used (a proxy blocking WebSockets, say). Your hub code is identical regardless of which transport wins.

Because the fallback is automatic, "use SignalR" rather than hand-rolling raw WebSockets gives you reconnection, transport negotiation, and a clean API for free.

Q: Why use SignalR instead of just polling the server?

Polling means every client repeatedly asks "anything new?", which is wasteful and laggy. SignalR keeps one connection open so the server pushes the instant there's news — fewer requests, near-instant updates. For live data with many clients, push wins.

No — WebSockets is one of the transports SignalR can use. SignalR adds automatic transport negotiation and fallback (to SSE or long polling), reconnection, groups, and a clean hub/client API on top, so you don't have to build all that yourself.

Q: Can I send a message to clients from outside a hub?

Yes. Inject IHubContext<YourHub> into a controller or background service and call _hub.Clients.All.SendAsync(...) . That's how a finished order or a scheduled job can push a notification with no client call to react to.

Q: Why shouldn't I store the ConnectionId to identify a user?

A ConnectionId is per-connection and changes on every reconnect, and one user may have several (phone, laptop). Identify users by their authenticated user id and let SignalR map it to connections with Clients.User(userId) .

Q: Do I need anything special to run SignalR across multiple servers?

Yes — a backplane (Redis or Azure SignalR Service). Without it, a broadcast only reaches clients on the same server instance that sent it, because each server only knows its own connections.

No blanks this time — just a brief and an outline. Build a ChatRoom that keeps a list of member handlers, lets members Join , and Broadcast s a message to every member so each prints its own receipt. It's the SignalR push model in miniature: one publisher, many subscribers. Run it and check the two receipts against the expected output.

Practice quiz

What is a Hub in SignalR?

  • A database table
  • A JavaScript library
  • The server-side class clients connect to and invoke methods on
  • A load balancer

Answer: The server-side class clients connect to and invoke methods on. A Hub is the server class clients connect to; each public method is something a client can call.

Which target sends a message to every connected client?

  • Clients.All
  • Clients.Caller
  • Clients.Others
  • Clients.Group(name)

Answer: Clients.All. Clients.All broadcasts to every connected client, like a radio broadcast.

What does Clients.Caller do?

  • Sends to everyone
  • Sends to a group
  • Disconnects the caller
  • Replies only to the client that made the call

Answer: Replies only to the client that made the call. Clients.Caller targets only the client that invoked the hub method.

What are SignalR Groups used for?

  • Storing user passwords
  • Private channels so only group members receive a broadcast
  • Speeding up the database
  • Replacing WebSockets

Answer: Private channels so only group members receive a broadcast. Groups are named sets of connections; Clients.Group(room) reaches only that group's members.

How do you add the current connection to a group?

  • Groups.AddToGroupAsync(Context.ConnectionId, room)
  • Clients.Group(room).Add()
  • Hub.Join(room)
  • Context.AddGroup(room)

Answer: Groups.AddToGroupAsync(Context.ConnectionId, room). Groups.AddToGroupAsync(Context.ConnectionId, room) adds this connection to a named group.

Why should you never store per-user state in hub fields?

  • Fields are read-only
  • Hubs can't have fields
  • A new hub instance is created per call and then discarded
  • It would leak memory

Answer: A new hub instance is created per call and then discarded. SignalR creates a fresh hub instance for every call and throws it away, so field state won't survive.

What is true about Context.ConnectionId?

  • It is the user's permanent id
  • It is per-connection and changes on every reconnect
  • It is the same for all clients
  • It is the user's email

Answer: It is per-connection and changes on every reconnect. ConnectionId is valid only for one connection and changes on reconnect; never persist it as a user identifier.

What is SignalR's main advantage over client polling?

  • It uses less code
  • It needs no server
  • It encrypts everything
  • The server pushes updates over one open connection instead of clients repeatedly asking

Answer: The server pushes updates over one open connection instead of clients repeatedly asking. SignalR keeps one connection open so the server pushes the instant there's news, avoiding wasteful polling.

What is the preferred, fastest transport SignalR negotiates first?

  • Long polling
  • WebSockets
  • Server-Sent Events
  • FTP

Answer: WebSockets. WebSockets is a true two-way persistent connection and is preferred; SignalR falls back to SSE then long polling.

When you run more than one server, what is needed so a broadcast reaches all clients?

  • A faster CPU
  • More memory
  • A backplane such as Redis or Azure SignalR
  • A second database

Answer: A backplane such as Redis or Azure SignalR. Without a backplane, a broadcast only reaches clients on the same server; Redis/Azure SignalR bridge instances.