Blazor Server Circuit Model: Architecture Note on State, Trust, and Scale
Architecture note on the Blazor Server circuit model: managing per-circuit state isolation, SignalR trust boundaries, operational checks, and the limits of in-memory state.
27 Oct 2025, 06:32 UTC

The problem with server-rendered interactivity
Building interactive UIs usually requires choosing between a heavy client-side SPA bundle or a stateless server-side model that requires full page reloads. Blazor Server provides a middle ground by maintaining the UI state on the server and sending only binary UI diffs to the browser via SignalR. However, this introduces a critical architectural constraint: the server must maintain a persistent, in-memory state for every active browser tab.
The essential takeaway is that a Blazor Server circuit is a volatile, in-memory session tied to a specific server node. It is not a durable storage mechanism for user data.
Requirements for interactive server-rendered UI
Blazor Server requires a persistent bidirectional channel. This is implemented via SignalR, which prefers WebSockets but can fall back to Server-Sent Events or long polling if the environment restricts WebSocket traffic.
Each browser tab creates a unique CircuitId. The circuit hosts the component render tree and all associated scoped services. Authentication is typically handled via cookies, which are validated during the initial SignalR negotiation phase.
This model assumes low network latency and a tolerance for brief reconnections. It is fundamentally incompatible with offline-first requirements.
Smallest suitable design
The minimal production design uses a single ASP.NET Core application where state is isolated via the Dependency Injection (DI) container. To ensure per-user isolation, state must be stored in Scoped services, which in Blazor Server are scoped to the circuit, not the HTTP request.
Example configuration in Program.cs:
builder.Services.AddServerSideBlazor();
// State is isolated to the specific browser tab/circuit
builder.Services.AddScoped<IUserSessionState, UserSessionState>();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
var app = builder.Build();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
To monitor the circuit lifecycle without modifying business logic, implement a CircuitHandler. This allows the server to track when users connect or disconnect:
public class CircuitLifecycleLogger : CircuitHandler
{
private readonly ILogger<CircuitLifecycleLogger> _logger;
public CircuitLifecycleLogger(ILogger<CircuitLifecycleLogger> logger) => _logger = logger;
public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken ct)
{
_logger.LogInformation("Circuit {CircuitId} opened", circuit.Id);
return Task.CompletedTask;
}
public override Task OnCircuitClosedAsync(Circuit circuit, CancellationToken ct)
{
_logger.LogInformation("Circuit {CircuitId} closed", circuit.Id);
return Task.CompletedTask;
}
}
Register this handler as a scoped service to ensure it tracks individual circuits.
Trust and data boundaries
The trust boundary is the SignalR transport layer. The browser is considered untrusted; the server remains the authoritative source of truth.
- Data Flow: Only UI diffs (DOM updates) and event notifications travel over the wire. The actual component logic and sensitive data stay in server memory.
- Exposure: Sensitive data is only exposed to the client if it is explicitly rendered into the HTML markup of a component.
- Persistence: Because circuits are volatile, any data that must survive a page refresh or server restart must be committed to a database or distributed cache immediately.
Operational checks
To verify the health and configuration of a Blazor Server deployment, perform the following checks:
- Network Inspection: Open Browser DevTools > Network tab. Filter for
/_blazor. Verify thenegotiaterequest completes and a WebSocket connection is established. - DI Audit: Ensure no
Scopedservices are injected intoSingletonservices, as this would cause a memory leak where circuit state is captured and never released. - Resource Monitoring: Track the
Active Circuit Countand memory usage per circuit. Each circuit consumes server RAM proportional to the complexity of the component tree. - Proxy Configuration: Ensure reverse proxies (like Nginx or Azure App Service) are configured to support WebSockets and have timeout settings that align with the SignalR keep-alive intervals.
Failure modes
Abrupt Disconnects: If the network drops, the framework attempts to reconnect. If the reconnection window expires, the circuit is disposed, and all unsaved in-memory state in scoped services is lost.
Server Restarts: A deployment or crash terminates all active circuits. Users will see a "Attempting to reconnect" message, but since the state was in-memory on the dead process, they must manually reload the page.
Scale-out Issues: Circuits are tied to the specific server node that created them. If a load balancer sends a reconnection request to a different node, the circuit is lost. Sticky sessions (session affinity) are mandatory for Blazor Server scale-out.
When to change the design
The circuit-based model should be abandoned in favor of Blazor WebAssembly or a hybrid approach when:
- Offline Capability: The app must function without a constant internet connection.
- Extreme Scale: The memory overhead of thousands of concurrent circuits exceeds server capacity.
- High Latency: Users are geographically distant from the server, causing perceptible lag in UI interactions (the "rubber-band" effect).
- Stateless Requirements: The architecture requires a completely stateless API surface for strict data residency or multi-region routing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.