Managing Blazor Server Circuit Resilience with Custom CircuitHandlers
Learn how to implement a custom CircuitHandler in Blazor Server to track user connectivity, prevent memory leaks, and customize the reconnection UI for a professional user experience.
27 Feb 2026, 17:47 UTC

The Problem: Silent Circuit Failures
Blazor Server relies on a SignalR connection to maintain a "circuit"—the stateful link between the browser and the server. When a user experiences a transient network drop or a server restart, the default behavior is to display a generic reconnection overlay. For professional applications, this is insufficient; you need to know exactly when a user disconnects, clean up server-side resources to prevent memory leaks, and provide a branded recovery experience.
Prerequisites
- .NET 6, 7, or 8 SDK
- A Blazor Server or Blazor Web App (Interactive Server mode) project
- Basic familiarity with Dependency Injection (DI) scoped services
Implementing a Custom CircuitHandler
A CircuitHandler allows you to hook into the lifecycle of a user's session. This is the primary mechanism for tracking active users and ensuring that background tasks are canceled when a circuit is permanently closed.
Step 1: Create the Handler Class
Inherit from CircuitHandler and override the lifecycle methods. Avoid blocking calls (like Task.Wait()) inside these methods, as they run on the circuit's synchronization context and can freeze the reconnection process.
using Microsoft.AspNetCore.Components.Server;
public class ConnectionTrackingHandler : CircuitHandler
{
private readonly ILogger<ConnectionTrackingHandler> _logger;
public ConnectionTrackingHandler(ILogger<ConnectionTrackingHandler> logger)
{
_logger = logger;
}
public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken)
{
_logger.LogWarning("Circuit {CircuitId} connection lost. User is now offline.", circuit.Id);
return Task.CompletedTask;
}
public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken)
{
_logger.LogInformation("Circuit {CircuitId} re-established.", circuit.Id);
return Task.CompletedTask;
}
public override Task OnCircuitClosedAsync(Circuit circuit)
{
_logger.LogCritical("Circuit {CircuitId} closed. Cleaning up resources.", circuit.Id);
// Perform final cleanup here, such as canceling background tokens
return Task.CompletedTask;
}
}
Step 2: Register the Handler
Register the handler in Program.cs. Because CircuitHandler is a scoped service, Blazor creates a new instance for every single browser tab opened by a user.
// Run this in Program.cs
builder.Services.AddScoped<CircuitHandler, ConnectionTrackingHandler>();
Customizing the Reconnection UI
The default "Attempting to reconnect..." message is often jarring. You can replace this by targeting the #components-reconnect-modal element in your main layout or App.razor.
Add the following CSS to your global stylesheet to style the different reconnection states (0: disconnected, 1: reconnecting, 2: failed):
/* Hide the default modal and style your own overlay */
#components-reconnect-modal {
display: none; /* Hide by default */
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
}
/* Show when the circuit is actually attempting to reconnect */
#components-reconnect-modal.components-reconnect-show {
display: flex;
align-items: center;
justify-content: center;
color: white;
}
Handling State Recovery
It is critical to understand that circuit state is volatile. If a user's connection is lost for longer than the server's disconnection timeout, the circuit is disposed. Any data stored in a Scoped service is wiped.
Comparison: Volatile vs. Durable State
| State Type | Storage Location | Survival Profile | Best Use Case |
|---|---|---|---|
| Circuit State | Scoped DI Service | Lost on circuit timeout | UI toggle states, temporary filters |
| Durable State | Database / Redis / ProtectedBrowserStorage | Survives browser restarts | Draft forms, User preferences, Shopping carts |
Verification and Testing
To verify that your resilience logic is working, perform these three tests:
- Transient Drop: Open Browser DevTools > Network tab. Set throttling to "Offline". Wait 5 seconds, then switch back to "No throttling". Check server logs for
OnConnectionDownAsyncfollowed byOnConnectionUpAsync. - Permanent Loss: While the app is offline, wait for the server-side timeout (default is usually 3 minutes, but can be configured). Refresh the page. Verify
OnCircuitClosedAsyncfired. - Resource Leak Check: Start a long-running task in a scoped service. Close the browser tab. Verify through your logs that the task was cancelled via the
OnCircuitClosedAsynchook.
Rollback Procedure
If the custom CircuitHandler causes performance degradation or blocking issues during reconnection, remove the builder.Services.AddScoped<CircuitHandler, ConnectionTrackingHandler>(); line from Program.cs and delete the handler class. The application will revert to the default SignalR lifecycle management.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.