Rehan Akbar
Rehan Akbar
All articles
September 28, 2026·5 min
  • #dotnet
  • #csharp
  • #signalr
  • #redis
  • #cloud
  • #architecture

Architecting a Sub-100ms Real-Time Identity Hub with .NET 8, SignalR, and Redis Pub/Sub

How we re-engineered a 5,000+ req/min enterprise identity workflow, cutting message delivery latency from 3–5 seconds down to sub-100ms with zero SLA breaches.

·
5 min read
·
··· reads

When scaling an enterprise identity system across 75,000+ active users, batch processing eventually hits an inescapable wall.

At the National University of Singapore, our legacy onboarding pipeline relied on a monolithic scheduled batch workflow. Nightly synchronization jobs regularly created 6-hour backlogs, and downstream services polling for state changes suffered from 3 to 5-second propagation delays.

Here is the architectural blueprint of how we re-engineered this system into a high-throughput, real-time event streaming hub using .NET 8 Clean Architecture, SignalR Core, and Redis Pub/Sub backplane — scaling to 5,000+ requests/minute with sub-100ms delivery latency.

Identity State Propagation Latency97.5% Faster
Before3,200 ms (Polling)
Optimized78 ms (SignalR + Redis)

Measured under sustained 5,000+ req/min concurrent load with zero packet drops.


The Core Problem: Polling vs. Event-Driven Hubs

In legacy enterprise setups, services frequently poll databases or token endpoints to check: "Has User X updated their multi-factor status or role permission?"

When 15+ downstream microservices poll for status changes simultaneously:

  1. Database Lock Contention: Index scans and repeated row queries consume compute.
  2. Artificial Lag: Changes are only as fresh as the polling interval (often 3–15 seconds).
  3. Wasted Compute: 99.4% of polling requests return 304 Not Modified.

To fix this, we inverted the flow: the identity engine produces real-time events via an in-memory non-blocking queue, broadcasts them across a Redis Pub/Sub backplane, and delivers instant state updates directly to connected clients over WebSockets via SignalR.

Real-Time Identity Hub Architecture
Identity Microservice.NET 8

Produces state change events into in-memory Channels queue.

Redis BackplaneRedis 7.2

Distributed Pub/Sub topic fanout across all active server nodes.

SignalR Core HubSub-100ms

Pushes WebSocket frames directly to authenticated clients.


1. Non-Blocking In-Memory Queuing with System.Threading.Channels

Under sudden traffic bursts (such as campus-wide morning authentication waves), writing directly to the database or external bus within the request thread risks thread-pool starvation.

We implemented an asynchronous producer-consumer channel in .NET 8 using bounded channels with backpressure:

csharp
// IdentityEventQueue.cs
using System.Threading.Channels;
 
public interface IIdentityEventQueue
{
    ValueTask EnqueueAsync(IdentityStateChangedEvent evt, CancellationToken ct = default);
    IAsyncEnumerable<IdentityStateChangedEvent> ReadAllAsync(CancellationToken ct = default);
}
 
public sealed class BoundedIdentityEventQueue : IIdentityEventQueue
{
    private readonly Channel<IdentityStateChangedEvent> _channel;
 
    public BoundedIdentityEventQueue(int capacity = 10_000)
    {
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait,
            SingleReader = true,
            SingleWriter = false
        };
        _channel = Channel.CreateBounded<IdentityStateChangedEvent>(options);
    }
 
    public async ValueTask EnqueueAsync(IdentityStateChangedEvent evt, CancellationToken ct = default)
    {
        ArgumentNullException.ThrowIfNull(evt);
        await _channel.Writer.WriteAsync(evt, ct);
    }
 
    public IAsyncEnumerable<IdentityStateChangedEvent> ReadAllAsync(CancellationToken ct = default)
    {
        return _channel.Reader.ReadAllAsync(ct);
    }
}
Why Bounded Channels?

Unbounded channels will happily consume memory until your container gets OOM-killed during a downstream outage. A bounded channel with BoundedChannelFullMode.Wait applies natural backpressure upstream, keeping memory utilization flat and deterministic.


2. Distributed Scale-Out with Redis Pub/Sub Backplane

When running multiple load-balanced web app instances, a client connected to Server A will not receive messages broadcast by Server B unless a shared backplane is configured.

We configured SignalR with a Redis backplane and custom channel partitioning:

csharp
// Program.cs - SignalR & Redis Service Registration
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
    options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB limit
})
.AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!, redisOptions =>
{
    redisOptions.Configuration.ChannelPrefix = "IdentityHub:Events";
    redisOptions.Configuration.DefaultDatabase = 0;
});
 
builder.Services.AddSingleton<IIdentityEventQueue, BoundedIdentityEventQueue>();
builder.Services.AddHostedService<IdentityEventBroadcasterWorker>();
 
var app = builder.Build();
 
app.MapHub<IdentitySyncHub>("/hubs/identity", options =>
{
    options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents;
});
 
app.Run();

3. Strongly-Typed SignalR Hub with JWT & Role-Based Claims

To prevent unauthorized access across sensitive identity feeds, every connection is validated against Azure AD / Entra ID tokens at handshake:

csharp
// IdentitySyncHub.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
 
public interface IIdentityClient
{
    Task ReceiveStateUpdate(IdentityStatePayload payload);
    Task ForceSessionRevocation(string reason);
}
 
[Authorize(AuthenticationSchemes = "Bearer")]
public class IdentitySyncHub : Hub<IIdentityClient>
{
    private readonly ILogger<IdentitySyncHub> _logger;
 
    public IdentitySyncHub(ILogger<IdentitySyncHub> logger)
    {
        _logger = logger;
    }
 
    public override async Task OnConnectedAsync()
    {
        var userId = Context.UserIdentifier;
        if (!string.IsNullOrEmpty(userId))
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}");
            _logger.LogInformation("Client connected: {ConnectionId} for User: {UserId}", Context.ConnectionId, userId);
        }
        await base.OnConnectedAsync();
    }
 
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        _logger.LogWarning(exception, "Client disconnected: {ConnectionId}", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

4. Benchmark Results: Before vs. After

Under peak load testing simulating 10,000 concurrent active WebSocket sessions publishing events at 5,000 requests/minute:

MetricLegacy Batch & Poll.NET 8 + SignalR + RedisDelta
P50 Delivery Latency3,240 ms42 ms-98.7%
P99 Delivery Latency5,800 ms88 ms-98.5%
Throughput Capacity850 req/min5,200 req/min+511%
Nightly Backlog Time6 hours0 min (Real-time)Eliminated
Database Compute CPU78% steady14% steady-82%
Production Lesson: Connection Draining

When deploying rolling updates in Kubernetes/Azure Container Apps, configure graceful SignalR connection termination. Abrupt pod kills will cause thousands of clients to reconnect simultaneously (Thundering Herd). Always set GracefulShutdownTimeout to at least 30 seconds to allow clients to re-negotiate connections progressively.


Key Takeaways

  1. Invert the Polling Habit: If downstream microservices need fresh state, use event streams and WebSocket hubs over HTTP polling.
  2. Backpressure is Mandatory: Unbounded queues are ticking memory leaks. Always bound channels with Wait or DropOldest.
  3. Decouple Ingestion from Broadcast: Let background workers drain channels into the Redis backplane asynchronously to keep the HTTP intake pipeline sub-10ms.

Have questions about SignalR Redis backplanes or high-throughput .NET 8 systems? Feel free to reach out via Email or connect on LinkedIn.

Weekly Engineering Notes

Enjoyed this technical deep dive?

Every Monday I share build logs, architecture trade-offs, and performance benchmarks from production systems. No spam, unsubscribe anytime.