Blazor Server Circuit Hangs — Scoped DbContext Pitfalls
Scoped DbContext survives Blazor Server renders, causing 30-second hangs after DB failover.
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
- Blazor lets you build interactive web UIs entirely in C#, eliminating JavaScript context-switching
- Components are .razor files mixing HTML + C# logic, self-contained and reusable
- Two-way binding with @bind syncs form fields to C# properties automatically
- Blazor Server runs C# on the server with SignalR; WebAssembly runs it in the browser
- Biggest mistake: thinking Blazor Server and WebAssembly need different coding patterns — they share the same component model
Imagine your TV remote. Normally you need a special remote (JavaScript) to change the channel on a specific TV (the browser). Blazor is like a universal remote that lets you use the same language you already know (C#) to control the browser directly. You stop learning two different languages and just use the one you love — C# does the clicking, the form-filling, and the live updates, all without writing a single line of JavaScript.
Blazor Server circuits hang when scoped DbContexts outlive their intended lifetime, blocking the single synchronization context that processes all UI events for a circuit. A database failover or slow query can freeze the circuit for 30 seconds or more, leaving users staring at a dead UI with no error message. The root cause is always the same: a DbContext registered as scoped that holds open connections and database locks across multiple user interactions. Fixing this requires changing how you manage DbContext lifetimes in Blazor components, using IServiceScopeFactory or IDbContextFactory<T> instead of direct injection.
What Blazor Server Circuit Hangs Really Are
Blazor Server runs your UI logic on the server over a persistent SignalR connection. Each client gets a circuit — a server-side session that holds component state, DI scopes, and the render tree. The circuit is the unit of failure: if it hangs, the user sees a frozen UI with no error. The root cause is often a scoped DbContext that outlives its intended lifetime. In Blazor Server, the scoped service lifetime matches the circuit, not the request. A DbContext registered as scoped lives for the entire circuit duration — minutes or hours, not milliseconds. This means connections stay open, transactions can stall, and locks accumulate. When the pool exhausts, the circuit blocks waiting for a connection, and the SignalR heartbeat fails. The server sees a disconnected client and tears down the circuit. The user gets a reconnect dialog. The fix is to register DbContext as transient or use IDbContextFactory<T> to create short-lived instances per operation. Never let a scoped DbContext span user interactions.
Blazor Components — The Single File That Does Everything
In Blazor, a component is a .razor file that contains three things in one place: HTML markup that defines the structure, a @code block that holds C# logic, and optional CSS isolation. This is the core mental model shift — stop thinking in terms of separate HTML files, JavaScript files, and C# controllers. A Blazor component is a self-contained unit of UI.
Every component has a lifecycle. When Blazor first renders a component it calls OnInitializedAsync — this is where you load data from an API or a database. After every parameter change, OnParametersSetAsync fires. The framework tracks which properties are bound to the UI and re-renders only the affected DOM nodes when those values change.
Components are also reusable by design. You define a ProductCard component once and drop <ProductCard Product="myProduct" /> anywhere in your app. Parameters flow down from parent to child via [Parameter] attributes, keeping your UI predictable and testable.
The key reason Blazor components beat raw Razor Pages for interactive UIs is that a Razor Page requires a full HTTP round-trip to reflect any change. A Blazor component updates in-place, in memory, without reloading the page — just like a React component does, but entirely in C#.
Two-Way Data Binding and Real-Time Form State with @bind
One of the first things you'll want to do in any web app is connect a form field to a C# object and have changes reflect immediately — in both directions. Type in the box, the C# property updates. Change the C# property in code, the box updates. Blazor calls this two-way binding and it's powered by the @bind directive.
Under the hood, @bind on an <input> is syntactic sugar for wiring up the value attribute to a property AND attaching an onchange event handler that writes the new value back. You can be explicit about timing using @bind:event="oninput" if you want the property to update on every keystroke rather than on blur.
This matters for real-world scenarios like live search boxes, character counters, or dependent dropdowns where changing one field should immediately filter the options in another. Without two-way binding you'd write the same plumbing logic yourself every time.
Beyond simple inputs, EditForm is Blazor's built-in form component that wraps DataAnnotations validation. You get client-side validation for free from the same [Required] and [Range] attributes you already use on your models — no duplicate validation rules in JavaScript.
Blazor Server vs Blazor WebAssembly — Choosing the Right Hosting Model
This is the question every Blazor developer faces first and gets wrong most often. Both hosting models use identical component syntax, so the choice is about execution environment and trade-offs, not about learning different APIs.
Blazor Server runs your C# components on the server. When a user clicks a button, a SignalR message travels from the browser to the server, the C# handler runs, the new virtual DOM diff is calculated on the server, and only the minimal HTML patch is sent back to the browser. The browser itself is just a thin rendering surface. This means startup is near-instant (no big download), you can access databases and file systems directly from component code, and your app works on browsers that don't support WebAssembly. The cost is latency sensitivity — every interaction needs a round-trip, and a server outage affects every connected user simultaneously.
Blazor WebAssembly (WASM) downloads the entire .NET runtime and your compiled DLLs to the browser on first load. After that, all component logic runs locally — zero server round-trips for UI interactions, offline capability, and it can be hosted as static files on a CDN. The trade-off is a larger initial download (typically 2-5 MB compressed) and no direct database access from component code.
A third option, Blazor United (now called Blazor Web App with Auto render mode in .NET 8+), starts components as Server for instant interactivity and then seamlessly transitions to WASM once the runtime downloads in the background — giving you the best of both.
dotnet counters before scaling; WASM is cheaper at scale for public apps.Dependency Injection and Service Lifetimes in Blazor Components
Blazor is built on top of ASP.NET Core's dependency injection container, so you get the same AddScoped, AddTransient, and AddSingleton lifetimes — but their behaviour in Blazor is subtly different from MVC and it catches people out.
In Blazor Server, a Scoped service lives for the duration of the SignalR circuit (the user's connection), not just a single HTTP request. This means a scoped service is effectively per-user and can hold state across multiple component renders and navigations — useful for shopping carts or user preferences. A Singleton is shared across all users on the server, so be careful with mutable state there.
In Blazor WebAssembly there's only one user per browser tab, so Scoped and Singleton behave identically in practice — they both live for the app's lifetime.
The @inject directive is how you pull services into a component. You can also use the [Inject] attribute in the @code block for constructor-style injection. The critical rule: never do real work in a component's constructor. Always use OnInitializedAsync for anything that involves services, because the DI container has fully resolved all dependencies by the time that lifecycle hook fires.
+= on an event, you must use -=.JavaScript Interop: When Blazor Needs the Browser APIs
Blazor covers 95% of UI needs in pure C#, but browsers have APIs that .NET doesn't expose natively: clipboard access, geolocation, camera/microphone, third-party JS libraries like Chart.js or Stripe elements. For these cases, Blazor provides JavaScript interop via the IJSRuntime service.
The pattern is simple: inject IJSRuntime into your component, then call InvokeAsync<T> to call a JavaScript function and get a result. You can also pass .NET object references to JavaScript and call back from JS to C# using DotNetObjectReference. This is how you integrate any JS library without leaving the Blazor ecosystem.
A common pitfall: forgetting that JavaScript interop calls in Blazor Server are serialized over SignalR — they add network latency. In WASM, they're synchronous calls within the same runtime. Always minimize the number of JS interop calls per render.
Another gotcha: JavaScript functions are called asynchronously. If you need to call JS during component initialization, use OnInitializedAsync and await the call. Do not block the constructor or OnInitialized with synchronous .Result — it will deadlock Blazor Server.
Forms and Validation — Why Your Blazor App Will Leak Data Without Proper Error Boundaries
Blazor's EditForm looks simple. You drop in some DataAnnotationsValidator, bind a model, and call it a day. That's how you ship a feature that silently accepts invalid data because you forgot the ValidationSummary or you used the wrong trigger for field validation. The WHY: Blazor validation is opt-in per model — not automatic. If your model doesn't inherit from IValidatableObject or lacks [Required] on a property, Blazor won't stop the submit. The HOW: Always pair DataAnnotationsValidator with ValidationSummary for global errors. Use OnValidSubmit and OnInvalidSubmit to separate success from failure paths. Never trust client-side validation alone; re-validate on the server inside your handler. The production trap is that Blazor Server can swallow validation exceptions in the circuit, leaving users with a broken form and no feedback. Add a try-catch around your submit handler that logs the exception and sets a UI error state. This isn't optional — it's how you stop a silent data corruption bug at 3 AM.
OnValidSubmit throws an unhandled exception, Blazor Server will terminate the circuit without telling the user. Wrap every handler in try-catch. Log the exception. Set a visible error on the UI. Your users won't thank you — but your on-call rotation will.Security — Why Your Blazor Server App Is One Circuit Away From Data Leakage
Blazor Server runs your C# on the server and sends UI diffs over a SignalR circuit. That circuit is an open pipe. If you don't authenticate every action, a user can call methods on a component they shouldn't see. The WHY: The circuit persists even after navigation. A compromised client can replay SignalR messages to execute component methods on a page the user left. The HOW: Use [Authorize] at the page level and inside every component that touches sensitive data. But that's not enough — you need to enforce authorization inside your service layer, because a user can manually invoke @onclick handlers from dev tools. Never trust the circuit's state to gate access. Add a CheckAccess() call at the start of every method that reads or writes user data. For roles, pass the current user's claims down to your service via AuthenticationStateProvider. The production trap is thinking that hiding a button with @if (user.IsInRole("Admin")) is enough. It's not — that only hides the UI. The method still exists on the server. If someone calls it, it runs. Verify on every server-side call.
@if(user.IsInRole("Admin")) is cosmetic. The server method is still callable via SignalR. Always re-verify authorization at the service layer using the server-side ClaimsPrincipal, not the client's state.Purpose — Blazor’s Real Job Isn’t What You Think
Most developers chase Blazor for hot-reload or component reuse. The real purpose is eliminating the JavaScript context switch. In Blazor Server, your C# code runs on the server and pushes UI diffs over a persistent SignalR connection. In Blazor WebAssembly, it runs in the browser on a .NET runtime. Both models free you from writing frontend code in a second language, but they solve different latency and data sensitivity problems. Blazor Server keeps business logic behind a firewall, while WebAssembly moves compute to the client. The purpose is not “build a SPA in C#” — it’s “keep your team speaking one language across the stack without sacrificing interactivity.” When your app needs real-time server state or strict data residency, Blazor Server is the choice. When you need offline capability or cheap static hosting, WebAssembly wins. Pick the purpose first, the hosting model second.
Prefer Visual Studio or the CLI?
Visual Studio gives you scaffolding wizards, integrated debugging, and a component designer. The CLI gives you speed, repeatability, and CI/CD compatibility. For beginners, Visual Studio reduces setup friction — one click creates a Blazor Server or WebAssembly project with proper launch profiles and NuGet restore. For teams shipping daily, the CLI is mandatory: dotnet new blazorserver -o MyApp produces identical output on every machine, no GUI required. The CLI also unlocks custom templates — you can bake in auth, service registrations, and CSS framework choices via -au Individual or -f net8.0. Visual Studio hides these flags behind menus, slowing repeatable builds. The real answer: use Visual Studio for exploration and one-off prototypes. Use the CLI for every project that will be deployed, reviewed, or shared.
dotnet list package --outdated after creation.Before You Start — The Real Blazor Prerequisites
Most tutorials gloss over this, but skipping the right foundation is why your Blazor app crashes in production within an hour. You need .NET 8 SDK or later, plus either Visual Studio 2022 (with the ASP.NET and web development workload) or the .NET CLI. For Blazor WebAssembly, you also need the wasm-tools workload. More critical: understand that Blazor Server demands a persistent WebSocket connection. If your network or proxy strips long-lived connections, every user action hangs. You also need a solid grasp of asynchronous C# — Task, ValueTask, and CancellationToken — because every component lifecycle method runs async by default. Finally, know that Blazor components are not self-contained islands; they inherit the HttpContext from the ASP.NET Core pipeline. Without understanding middleware order and circuit lifetime, your authentication and session state will leak data across users. Install the tools, then study the circuit lifecycle before writing your first component.
wasm-tools workload for WebAssembly. Without it, the AOT compilation fails silently. Always run dotnet workload install wasm-tools before your first publish.Getting Started — Beyond the 'dotnet new' Illusion
The default Blazor templates promise a working app in seconds, but they hide critical configuration choices that bite you later. When you run dotnet new blazorserver or blazorwasm, you get a scaffold with a Program.cs that wires up services, authentication, and the circuit handler. The real question isn't 'how' but 'why' those defaults exist. For Blazor Server, AddRazorComponents(). configures the circuit hub. If you don't understand that AddInteractiveServerComponents()CircuitOptions.DisconnectedCircuitMaxRetained defaults to 100, your app will silently drop users under load. For WebAssembly, the template adds AddBlazorWebAssemblyServices() which inlines all DLLs into the initial download. That kills your first paint time. The correct approach: override these defaults explicitly. Set CircuitOptions.DetailedErrors = true during development. For WebAssembly, lazy-load assemblies using the LazyAssemblyLoader service. The template is a starting line, not a finish line — always audit the Program.cs before adding your first component.
DisconnectedCircuitMaxRetained of 100 consumes memory per user. Under 1,000 concurrent users, that's 100,000 circuits retained — a guaranteed OOM crash. Lower it to 50 or use DisconnectedCircuitRetentionPeriod.Production Outage: Blazor Server Circuit Holds After DB Failover
- In Blazor Server, scoped services live across multiple renders — don't assume they reconnect automatically after transient failures.
- Always use CancellationToken with async calls in Blazor components to prevent hanging circuits.
- Add a health check endpoint that validates the SignalR circuit and database connectivity, and alert if >5% of circuits are in pending state.
docker logs on the SignalR hub server. Look for Microsoft.AspNetCore.SignalR warnings. Verify WebSocket connections are stable and not blocked by proxy/firewall.OnInitializedAsync correctly. The method is called each time the component is rendered, including after user navigation. If you cache data in a scoped service, ensure it's cleared when the user's circuit reconnects.StateHasChanged() called but UI doesn't updateStateHasChanged inside a Blazor event handler or lifecycle method. If called from an external event (timer, service callback), wrap it in await InvokeAsync(StateHasChanged). Otherwise the update is silently ignored.Scoped in Blazor Server lives for the entire SignalR circuit. If you inject a large data context or cache, it stays in memory for every user. Move to transient for per-call operations or implement IDisposable to release resources.Open browser DevTools → Network → WS tab. Filter by 'negotiate'. Look for 101 status code.On server: `kubectl logs deployment/blazor-server -n production | grep "SignalR"`services.AddSignalR(o => o.KeepAliveInterval = TimeSpan.FromSeconds(15))Key takeaways
StateHasChanged() — when triggering a re-render from a callback, timer, or background task. And always implement IDisposable to unsubscribe from events. These two rules eliminate the most common Blazor bugs in production.Common mistakes to avoid
4 patternsCalling StateHasChanged after every await
Fetching data in OnInitialized instead of OnInitializedAsync
GetAwaiter().GetResult() deadlocks Blazor Server's synchronisation context. Always await async calls inside OnInitializedAsync.Not implementing IDisposable when subscribing to events or timers
Dispose() method, unsubscribe every event and stop every timer. In .razor files, declare this with '@implements IDisposable' at the top and add a Dispose() method in the @code block.Using Blazor Server for a public-facing consumer app without proper capacity planning
Interview Questions on This Topic
What is the difference between Blazor Server and Blazor WebAssembly, and what are the specific production scenarios where you would choose one over the other?
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.
That's ASP.NET. Mark it forged?
10 min read · try the examples if you haven't