How to Build a Real-Time Notification System in ASP.NET Core 10 Using SignalR and Redis (2026)
Use SignalR for live connections and a Redis backplane when more than one ASP.NET Core instance serves the same application. Redis forwards live messages between instances so a notification sent on server A can reach an authenticated client connected to server B. It does not persist unread messages. If the notification has business value, store it in the application database and use SignalR to tell the client there is something new.
How notifications cross instances
A hub handles connected clients, and an injected hub context lets an HTTP endpoint or background service send messages without keeping a hub instance. Clients.User sends to all live connections with the same SignalR user identifier. A group instead targets the current connections that explicitly joined it. Neither a group name nor membership is a substitute for permission checks.
SignalR Redis backplane · SignalR authentication and authorization
Prepare two API instances and a client
Install the .NET 10 SDK and start a private Redis instance. For a local-only Docker test, use docker run --rm -p 127.0.0.1:6379:6379 redis:8. Create a dotnet new web project, then add the stable .NET 10 package references Microsoft.AspNetCore.SignalR.StackExchangeRedis and Microsoft.AspNetCore.Authentication.JwtBearer. Create a separate console project and add Microsoft.AspNetCore.SignalR.Client from the same .NET major version.
Set Auth__Authority and Auth__Audience to your trusted identity issuer and API audience. This sample uses one issuer, with a unique validated subject claim per user. A trusted issuer must issue the API’s roles claim with the Notification.Send role for senders. If your identity provider uses a different claim contract, explicitly configure the policy to match it. Set ConnectionStrings__Redis to the same Redis endpoint for both servers. Use an isolated Redis endpoint and a distinct channel prefix when sharing infrastructure with another app.
Configure the API and authenticated hub
The notification route checks the sender’s role and restricts the payload to a short text notice. In production, also validate the recipient against the sender’s tenant and action. If you trust multiple issuers, compose a stable identifier from issuer and subject or resolve an internal account ID. Do not let the client choose the group’s identity segment. This hub uses the authenticated connection’s subject. The sample’s HTTP route sends directly to a user. The join method separately demonstrates a per-user group pattern.
Connect and exercise both instances
Start one API on port 5101 and one on 5102 with the same authentication and Redis configuration. Use HTTPS endpoints for real tokens. Run the console with HUB_URL pointing to the second instance and HUB_TOKEN set to a valid short-lived user access token. Send a POST to /internal/notifications/<subject> on the first instance. Authenticate with a sender token bearing the required role and send {"text":"Order 101 has shipped."} as JSON. Expect HTTP 202 and the message on the console connected to the second instance. A different subject should not receive it. An anonymous sender should receive 401 and a signed-in caller without the role should receive 403. A local smoke test using Redis 8 and a fixture OIDC issuer delivered a notice across two replicas and returned 403 for a sender without the required role. Reconnection, Redis failover, and a production identity provider remain test steps.
Automatic reconnect retries a lost established connection. Handle an initial start failure separately. A reconnect can produce a new connection, which is why the handler joins again. In production, refresh access tokens via the client’s token provider instead of keeping a fixed token. Browser WebSocket and SSE transports can carry an access token in the query string when headers are unavailable. Restrict extraction to the hub path if you support browsers and remove tokens from request logs.
Delivery and scaling decisions
| Requirement | Design |
|---|---|
| Live notification | SignalR user delivery via the shared Redis backplane. |
| Catch-up after reconnect | Persist notices and let the client query a cursor or unread list. |
| Delivery acknowledgment | Give notices IDs and record a separate client or application acknowledgment. |
| Multiple app instances | Use an affinity-aware proxy for the default transport negotiation flow. |
| Managed connection scale | Compare Azure SignalR Service with operating the app connections and Redis yourself. |
Keep Redis close to the application servers because backplane traffic crosses it. Protect the endpoint with private networking, authentication and TLS in production. If Redis is down, messages sent during the outage are not buffered by the backplane for replay. Document this boundary before promising delivery. See the ASP.NET PB Redis caching guide for a separate Redis usage pattern.
Troubleshooting and FAQs
Messages arrive only on the local instance. Confirm both replicas use the same backplane endpoint and channel prefix, then look for Redis connectivity or TLS errors. Verify that the recipient’s SignalR identifier matches the subject string exactly.
Why does the client get 404 during negotiation? Check proxy paths, hub route and session affinity. HTTP negotiation and the transport upgrade must follow the requirements of your deployment topology.
Does 202 mean the client read the message? It means the endpoint completed its send attempt. Track delivery or user acknowledgment separately if required.
Should every event go through SignalR? Keep durable state changes in a database or broker. Use SignalR for live updates that can be recovered by reading current state after a reconnect.
Comments
Post a Comment