Sending Instant Notifications with SignalR

August 20, 2026Sending Instant Notifications with SignalR
Sarah Dutkiewicz

Sarah Dutkiewicz, Senior Trainer

So far in [our SignalR for Blazor tour](/series/signalr-for-blazor, we’ve covered the foundations (part 1), connections and hubs (part 2), collaborative features with groups (part 3), and streaming data updates (part 4). That leaves the third use case we previewed way back in part 1: notifications. This post is for developers who want to implement real-time notification systems, and our goal is to show how to efficiently build notification mechanisms with SignalR in a Blazor app — what kinds of notifications exist, how to design the hub for sending and receiving, and a working notification center you can try yourself.

Note: All code demos for this series are available in nimblepros/signalr-code-demo.

Notification Types: More Than One Channel 📬

Before writing any code, it’s worth getting clear on what “notification” means, because the channel you pick changes everything. Notifications come in several flavors:

  • In-app, real-time notifications — the ones this post is about. The user is actively using the app, and a toast or banner appears instantly when something happens. SignalR is the right tool here: the app has an open connection, and the server pushes the notification the moment it occurs.
  • Email — useful when the user isn’t in the app. Email is inherently asynchronous; an email service (like SendGrid or an SMTP provider) handles delivery. SignalR doesn’t send email, but your business logic can both send the email and push an in-app notification for the same event.
  • SMS / text messages — like email, delivered by an SMS provider, async, and meant for when the user is away. SignalR plays the same supporting role.
  • Mobile and browser push notifications — delivered through platform push services (APNs, FCM, Web Push) that can reach a device even when the app or page is closed. Again, not a SignalR job.

The key distinction: SignalR delivers instantly, but only while the user is connected to your app. The other channels exist to reach users who aren’t. A common pattern is a single business event that fans out to several channels at once — an in-app toast and an email, say — and SignalR handles just the real-time leg.

Hub Method Design: Sending & Receiving 📨

Now let’s talk about how a notification moves through a SignalR app. There are two directions, and they have different design considerations.

Receiving notifications

On the client, a notification is just a server-to-client message. The component subscribes to a method name:

_connection.On<AppNotification>("NotificationReceived", notification =>
{
    // Show a toast or add it to a list
});

On the server, the interesting part is targeting: a notification is usually meant for one specific user, not everyone. We explored two targeting mechanisms earlier in the series — Clients.User() requires authentication (SignalR maps the logged-in user to their connections), while groups let you target a named set of connections. For our demo we’ll use groups keyed by user ID, which keeps the example runnable without setting up authentication. In a real app with login, the idiomatic approach is [Authorize] on the hub and Clients.User(userId), which SignalR resolves through the IUserIdProvider.

Sending notifications

Notifications originate from two places, and each maps to a different server-side pattern:

  • From a client. One user does something that should notify another (say, mentions a teammate). A hub method receives the request and pushes to the target. This is a normal client-to-server → server-to-client flow.
  • From the system. This is the one that surprises people. Most notifications don’t start with a client calling the hub — they start in a background job, a business service, or an API endpoint. To push from outside a hub, you inject IHubContext<THub> and call it directly. This is the workhorse of real notification systems.

Here’s the full picture:

  flowchart LR
    subgraph Sources [Notification sources]
        A[Background job / simulator]
        B[Another user]
        C[Business logic event]
    end
    D[NotificationHub]
    E[Target user's connected clients]
    A -->|IHubContext push| D
    B -->|hub method| D
    C -->|IHubContext push| D
    D -->|SendAsync| E
  

One design note: notifications should be efficient and polite. Don’t fire a message per event if events arrive in bursts — batch or coalesce, and avoid flooding a user with dozens of near-identical toasts. If your app can generate a lot of notifications, consider rate-limiting or a per-user queue that the UI drains gradually.

Code Example: A Notification Center 👨‍💻

Let’s build a small notification center. It will demonstrate both sending patterns: a background service that generates system notifications, and a user-to-user notification. We’ll scope delivery per user so that each user’s tab only sees their own notifications.

First, the model. Create a file called AppNotification.cs:

namespace SignalRForBlazorDemo.Models;

public record AppNotification(Guid Id, string Message, DateTimeOffset Timestamp);

Next, the hub. Create a file called NotificationHub.cs:

using Microsoft.AspNetCore.SignalR;
using SignalRForBlazorDemo.Models;

namespace SignalRForBlazorDemo.Hubs;

public class NotificationHub : Hub
{
    // A client calls this to register for a user's notifications. We use a
    // group named after the user so delivery is scoped to that user's tabs.
    // (With authentication you'd typically use [Authorize] and Clients.User.)
    public async Task JoinUserAsync(string userId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, userId);
    }

    // One user sends an instant notification to another user.
    public async Task SendToUserAsync(string targetUserId, string message)
    {
        var notification = new AppNotification(
            Guid.NewGuid(), message, DateTimeOffset.UtcNow);

        // Only connections in the target user's group receive this.
        await Clients.Group(targetUserId).SendAsync("NotificationReceived", notification);
    }
}

Register the hub in Program.cs:

app.MapHub<NotificationHub>("/notificationhub");

Now for the system-generated side. Create a background service that periodically simulates events and pushes them through IHubContext — no client involved. Create a file called NotificationSimulator.cs:

using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using SignalRForBlazorDemo.Hubs;
using SignalRForBlazorDemo.Models;

namespace SignalRForBlazorDemo.Services;

public class NotificationSimulator(
    ILogger<NotificationSimulator> logger,
    IHubContext<NotificationHub> hubContext) : BackgroundService
{
    private static readonly string[] Messages =
    [
        "Your report is ready to view.",
        "A teammate mentioned you in a comment.",
        "New activity in a project you follow.",
        "Your task was marked complete."
    ];

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var random = new Random();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Simulate waiting for real events to happen.
            await Task.Delay(TimeSpan.FromSeconds(8), stoppingToken);

            var notification = new AppNotification(
                Guid.NewGuid(),
                Messages[random.Next(Messages.Length)],
                DateTimeOffset.UtcNow);

            // Push to the "user1" channel via IHubContext. In a real app this
            // could come from a queue consumer, a job, or business logic.
            await hubContext.Clients.Group("user1")
                .SendAsync("NotificationReceived", notification);

            logger.LogInformation("Pushed notification {NotificationId}", notification.Id);
        }
    }
}

Register it as a hosted service:

builder.Services.AddHostedService<NotificationSimulator>();

Finally, the component that shows the notifications. Create a file called NotificationCenter.razor:

@using Microsoft.AspNetCore.SignalR.Client
@implements IAsyncDisposable

<div>
    <label>User ID: <input value="@_userId" @oninput="OnUserIdInput" /></label>
    <button @onclick="JoinAsync">Join</button>
</div>

<div>
    <label>Send to: <input value="@_targetUserId" @oninput="OnTargetInput" /></label>
    <button @onclick="SendNotificationAsync">Send Notification</button>
</div>

@if (_errorMessage is not null)
{
    <p class="text-red-600">@_errorMessage</p>
}

<ul>
    @foreach (var notification in _notifications)
    {
        <li>@notification.Timestamp.ToString("HH:mm:ss") - @notification.Message</li>
    }
</ul>

@code {
    private HubConnection? _connection;
    private string _userId = "user1";
    private string _targetUserId = "user2";
    private string? _errorMessage;
    private readonly List<AppNotification> _notifications = [];

    protected override async Task OnInitializedAsync()
    {
        // Build the connection to our NotificationHub.
        _connection = new HubConnectionBuilder()
            .WithUrl("/notificationhub")
            .WithAutomaticReconnect()
            .Build();

        // Handle notifications pushed from the server.
        _connection.On<AppNotification>("NotificationReceived", notification =>
        {
            _notifications.Insert(0, notification);
            StateHasChanged();
        });

        await _connection.StartAsync();
        await JoinAsync();
    }

    private async Task JoinAsync()
    {
        if (_connection is null)
        {
            return;
        }

        // Register this connection for the current user's notifications.
        await _connection.InvokeAsync("JoinUserAsync", _userId);
    }

    private async Task SendNotificationAsync()
    {
        if (_connection is null)
        {
            return;
        }

        try
        {
            // Ask the hub to deliver an instant notification to the target user.
            await _connection.InvokeAsync(
                "SendToUserAsync", _targetUserId, "You have a new message!");
        }
        catch (Exception ex)
        {
            _errorMessage = $"Failed to send: {ex.Message}";
        }
    }

    private void OnUserIdInput(ChangeEventArgs e) =>
        _userId = e.Value?.ToString() ?? string.Empty;

    private void OnTargetInput(ChangeEventArgs e) =>
        _targetUserId = e.Value?.ToString() ?? string.Empty;

    public async ValueTask DisposeAsync()
    {
        // Always clean up the connection when the component goes away.
        if (_connection is not null)
        {
            await _connection.DisposeAsync();
        }
    }
}

Here’s the whole flow:

  sequenceDiagram
    participant S as NotificationSimulator (background)
    participant H as NotificationHub
    participant U1 as User 1's browser
    participant U2 as User 2's browser
    U1->>H: JoinUserAsync("user1")
    U2->>H: JoinUserAsync("user2")
    loop every 8 seconds
        S->>H: IHubContext push to "user1" group
        H-->>U1: NotificationReceived(notification)
    end
    U1->>H: SendToUserAsync("user2", "You have a new message!")
    H-->>U2: NotificationReceived(notification)
  

To try it: run the app and open two tabs, one with User ID user1 and one with user2 (click Join in each). The user1 tab receives a simulated notification every few seconds from the background service. Click Send Notification in the user1 tab, and the user2 tab receives it instantly. Each tab only ever sees notifications for its own user — the isolation comes from the groups.

What’s Next? 🎬

Let’s recap what we covered: the fundamentals of real-time communication and SignalR, connections and hubs, collaborative features with groups, streaming data updates, and now instant notifications. Each post built on the last, and together they cover the three real-time use cases we promised in part 1. If you want to go further, a natural next step is production hardening: adding authentication and per-user targeting with Clients.User(), configuring the Azure SignalR Service for scale, and combining SignalR with other notification channels like email and push. The next post will cover best practices, patterns, and anti-patterns.

Resources 📚