Skip to main content

    Advanced Track

    Real-Time Communication with 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.

    What You'll Learn

    • Explain why server push (SignalR) beats client polling for live data
    • Write a Hub and call its methods from a JavaScript client
    • Target messages with Clients.All, Clients.Caller, and Clients.Group
    • Use groups as private channels and add/remove connections at runtime
    • Handle the connection lifecycle (connect, disconnect, reconnect)
    • Understand the WebSocket / SSE / long-polling transport fallback

    📡 Real-World Analogy

    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.

    Push vs Polling — the core idea

    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.

    • 🔁 Polling — client asks every few seconds; lots of empty requests; updates lag.
    • 📡 SignalR push — one open connection; server sends only when there's news; near-instant.

    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.

    📊 Targeting Clients — who receives a message

    TargetWho gets itTypical use
    Clients.AllEvery connected clientGlobal broadcast, announcements
    Clients.CallerOnly the client that calledAcknowledgements, replies
    Clients.OthersEveryone except the caller"User joined / is typing"
    Clients.Group(name)Members of that groupChat rooms, channels
    Clients.Client(id)One connection by idDirect/private message
    Clients.User(userId)All of one user's connectionsNotify a person on every device

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

    Worked example: a ChatHub that broadcasts

    Read every comment. This needs ASP.NET to run, so study it — you'll drill the same idea below.

    Try it Yourself »
    C#
    using Microsoft.AspNetCore.SignalR;
    
    // ChatHub.cs — a Hub is the server class clients connect to.
    // Each PUBLIC method here is something a client can call over the wire.
    // NOTE: SignalR needs an ASP.NET Core host, so this won't run in the fiddle —
    // study it, then practise the same PUSH idea in plain C# further down.
    public class ChatHub : Hub
    {
        // A client calls this; the server then PUSHES to every connected client.
        public async Task SendMessage(string user, string message)
        {
     
    ...

    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:

    🎯 Your turn: a broadcast hub stand-in

    Subscribe two handlers and Broadcast to all — the Clients.All idea, runnable.

    Try it Yourself »
    C#
    using System;
    using System.Collections.Generic;
    
    // 🎯 YOUR TURN — SignalR can't run here, so we model the SAME idea in
    // plain C#. A "hub" keeps a list of subscriber handlers and broadcasts
    // a message to every one of them — exactly like Clients.All.SendAsync.
    
    class MiniHub
    {
        // Every subscriber is an Action<string> — a handler that takes a message.
        private readonly List<Action<string>> _subscribers = new();
    
        // Subscribe = the client "tunes in".
        public void Subscribe(Action<
    ...

    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.

    Worked example: a RoomHub with groups

    See join, send-to-group, and leave. Only group members receive a Group broadcast.

    Try it Yourself »
    C#
    using Microsoft.AspNetCore.SignalR;
    
    // Groups are PRIVATE channels — only members of a group receive a
    // Group broadcast. Think of a conference call inside the radio station.
    public class RoomHub : Hub
    {
        // Add THIS connection to a named group (e.g. a chat room).
        public async Task JoinRoom(string room)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, room);
            // Tell only this room that someone arrived.
            await Clients.Group(room).SendAsync("ReceiveMessage",
    ...

    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:

    🎯 Your turn: add groups to the hub

    Map group → subscribers and send to one group only — the Clients.Group idea.

    Try it Yourself »
    C#
    using System;
    using System.Collections.Generic;
    
    // 🎯 YOUR TURN — add GROUPS to the hub: a Dictionary mapping a
    // group name to the list of subscribers in it. Sending to a group
    // reaches only that group (the Clients.Group idea).
    
    class GroupHub
    {
        // group name -> the handlers subscribed to that group
        private readonly Dictionary<string, List<Action<string>>> _groups = new();
    
        // Add a handler to a named group (creating the group if it's new).
        public void JoinGroup(string group
    ...

    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.

    Worked example: connecting from JavaScript

    Build the connection, listen for ReceiveMessage, then invoke a server method.

    Try it Yourself »
    C#
    // JavaScript client — the browser end of the connection.
    // Install:  npm install @microsoft/signalr
    import * as signalR from "@microsoft/signalr";
    
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/hubs/chat")                 // must match app.MapHub(...)
        .withAutomaticReconnect()              // auto-retry if the link drops
        .build();
    
    // Register handlers BEFORE start() — the name must match the server's
    // SendAsync("ReceiveMessage", ...) exactly, or the message is
    ...

    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.

    Worked example: connect & disconnect hooks

    Track presence with the lifecycle overrides — not with hub fields.

    Try it Yourself »
    C#
    using Microsoft.AspNetCore.SignalR;
    
    // A Hub instance is created PER CALL and thrown away — never store state
    // in fields. Track connections with the lifecycle overrides instead.
    public class PresenceHub : Hub
    {
        // Runs when a client opens the connection.
        public override async Task OnConnectedAsync()
        {
            // Context.ConnectionId is a fresh id for THIS connection only.
            await Clients.Others.SendAsync("UserJoined", Context.ConnectionId);
            await base.OnConnectedAsyn
    ...

    🔎 Deep Dive: transports & the automatic fallback

    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.

    • 🥇 WebSockets — a true two-way persistent connection. Fastest and lowest overhead; the preferred choice.
    • 🥈 Server-Sent Events (SSE) — server-to-client streaming only; client-to-server uses separate requests. Used when WebSockets aren't available.
    • 🥉 Long Polling — the client holds an HTTP request open until the server has something, then immediately reopens it. The universal fallback that works almost everywhere.

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

    Pro Tips

    • 💡 Keep hub methods fast and non-blocking: a hub processes messages on a connection one at a time, so a slow method stalls that client. Offload heavy work to a background service and await real I/O — never block.
    • 💡 Use a Redis (or Azure SignalR) backplane when you run more than one server: builder.Services.AddSignalR().AddStackExchangeRedis("…"); so a broadcast from one server reaches clients connected to the others.
    • 💡 Push from outside the hub with IHubContext<THub>: inject it into a controller or background job to send messages when there's no client call to react to.
    • 💡 Prefer strongly-typed hubs (Hub<IClient>) to replace magic strings like "ReceiveMessage" with compiler-checked interface methods.
    • 💡 Send small payloads: broadcast an id or a delta, not a megabyte of JSON to thousands of clients at once.

    Common Errors (and the fix)

    • No authentication on the hub: a hub method is a public endpoint — anyone who can reach the URL can call it. Protect it with [Authorize] on the hub (or method) and read the user from Context.User; never trust a user id passed in as an argument.
    • Blocking inside a hub method: calling Thread.Sleep(...) or .Result/.Wait() on a task freezes that connection's message processing and can starve the thread pool. Use await Task.Delay(...) and async I/O instead.
    • Assuming a single server / sticky sessions: behind a load balancer, two clients may land on different servers, so Clients.All only reaches one server's clients. Add a backplane (Redis or Azure SignalR) so broadcasts cross all instances.
    • Huge broadcasts: sending large messages to Clients.All with thousands of connections floods the network and memory. Send small payloads, throttle the rate, and target groups instead of everyone where you can.
    • Client handler name doesn't match: SendAsync("ReceiveMsg", …) on the server but connection.on("ReceiveMessage", …) on the client — the names differ, so the message is silently dropped. Keep them identical (typed hubs prevent this).

    📋 Quick Reference

    TaskCodeNotes
    Define a hubclass ChatHub : Hub { ... }Server class clients connect to
    Broadcast to allClients.All.SendAsync("M", x)Every connected client
    Reply to callerClients.Caller.SendAsync(...)Only the sender
    Join a groupGroups.AddToGroupAsync(id, "room")id = Context.ConnectionId
    Send to a groupClients.Group("room").SendAsync(...)Members only
    Map the hubapp.MapHub<ChatHub>("/hubs/chat")In Program.cs
    JS: listenconnection.on("M", fn)Before start()
    JS: call serverconnection.invoke("SendMessage", …)Calls a hub method

    Frequently Asked Questions

    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.

    Q: Is SignalR the same as raw WebSockets?

    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.

    Mini-Challenge: a Chat Room

    No blanks this time — just a brief and an outline. Build a ChatRoom that keeps a list of member handlers, lets members Join, and Broadcasts 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.

    🎯 Mini-Challenge: broadcast to a chat room

    Two members join; one broadcast produces two receipts.

    Try it Yourself »
    C#
    using System;
    using System.Collections.Generic;
    
    // 🎯 MINI-CHALLENGE: a tiny chat-room model (the SignalR push idea in plain C#)
    // 1. Make a ChatRoom class with a private List<Action<string>> called members.
    // 2. Add Join(Action<string> onMessage): add the handler to members.
    // 3. Add Broadcast(string from, string text): loop over members and call
    //    each handler with a line like  $"{from}: {text}".
    // 4. In Main: create a room, Join TWO members that print what they receive,
    //    then Br
    ...

    🎉 Lesson Complete

    • ✅ SignalR lets the server push updates over one open connection — no polling
    • ✅ A Hub is the server class; its public methods are what clients invoke
    • ✅ Target messages with Clients.All, Caller, Others, Group, and Client
    • Groups are private channels — join/leave at runtime; broadcast to members only
    • ✅ The lifecycle (OnConnectedAsync/OnDisconnectedAsync) tracks presence; never store state in hub fields
    • ✅ Transports fall back automatically: WebSockets → SSE → long polling
    • ✅ Secure hubs with [Authorize], keep methods non-blocking, and add a backplane for multiple servers
    • Next lesson: WebSockets — the low-level transport SignalR is built on

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service