Optimizing Blazor Server Scalability on Windows Hosting
Blazor Server has a reputation for "not scaling." It's mostly a myth — repeated by people who benchmarked it wrong or never tuned the two things that actually matter: circuit memory and SignalR throughput. Get those right and a single dedicated IIS Application Pool on Windows comfortably carries hundreds to low-thousands of concurrent interactive users.
This guide is the practical playbook we recommend on Adaptive Web Hosting: how the circuit model consumes RAM, how to right-size your app pool, how to tune SignalR and the .NET 10 garbage collector, and how to keep IIS worker processes warm so your app stays fast under real load. Everything here targets .NET 10 LTS on real Windows Server 2022 + IIS 10.
1–4 GBDedicated app-pool RAM
.NET 10LTS through 2028
1 poolNo sticky-session backplane
How Blazor Server actually scales
Every connected user holds a circuit: a small amount of server-side state (your component tree and its fields) plus an open SignalR connection. The browser sends UI events up the wire; the server re-renders the affected components and ships back a diff. There is no per-request controller spin-up and no client-side state machine — which is exactly why it feels so productive to build.
Blazor Server scalability is a memory problem, not a CPU problem. Capacity = (app-pool RAM − runtime baseline) ÷ (RAM per circuit). Lower the per-circuit cost and you raise the ceiling — without paying for a bigger plan.
An idle circuit is cheap — on the order of 250 KB. The number that bites is active state: a component that captures a 5 MB list, a chart with thousands of points, or a cache held in a field. Multiply that by every connected user and you understand why two identical apps can have wildly different concurrency limits. The capacity math below assumes lean components; we'll tighten them in a moment.
These are deliberately conservative back-of-envelope figures (leaving headroom for the .NET runtime, your data, and GC slack) — your real number depends entirely on component design. For a rigorous per-app estimate, walk through our Blazor Server RAM sizing guide, which measures a real circuit instead of guessing.
Right-size the app pool first
Before any clever tuning, make sure you have the RAM to work with. On Adaptive Web Hosting every site runs in a dedicated IIS Application Pool — your worker process never competes with another tenant for memory, which is the whole reason the numbers above are predictable rather than aspirational.
Circuits live in your worker process's heap. On oversold shared hosting, a noisy neighbor's memory spike can trigger pool recycles that drop every one of your live circuits at once. A dedicated pool with guaranteed RAM (1 GB Developer, 2 GB Business, 4 GB Professional) removes that variable entirely.
Vertical scaling — moving to a higher tier for more app-pool RAM — is the simplest lever and usually the right first move. Compare the tiers on the ASP.NET hosting plans page, or read the full .NET 10 LTS hosting guide for how the runtime is provisioned.
Tune SignalR for throughput and stability
Blazor Server's transport is SignalR, and its defaults are conservative. Three settings matter most under load. Configure them in Program.cs:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddHubOptions(options =>
{
// Default is 32 KB. Raise it only if you push large render
// batches or stream long responses through the circuit.
options.MaximumReceiveMessageSize = 64 * 1024;
// How often the server pings the client. Lower = faster
// disconnect detection, slightly more chatter.
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
// How long before a silent client is considered gone and its
// circuit torn down. Must be at least 2x KeepAliveInterval.
options.ClientTimeoutInterval = TimeSpan.FromSeconds(40);
});
builder.Services.AddServerSideBlazor()
.AddCircuitOptions(options =>
{
// NEVER true in production — leaks exception detail to clients.
options.DetailedErrors = false;
// Memory held for clients that drop and may reconnect. Under
// heavy churn, trimming these frees RAM faster.
options.DisconnectedCircuitMaxRetained = 100;
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3);
});
Aggressive ClientTimeoutInterval values cause healthy users on flaky mobile networks to lose their circuit and see the "reconnecting" overlay. If your users complain about drops, that's usually the cause — see our deep dive on why Blazor Server SignalR connections keep dropping before tightening anything.
The DisconnectedCircuit* options are the quiet memory win: by default the server keeps up to 100 dropped circuits in memory for 3 minutes so a reconnecting user resumes exactly where they were. Under high churn (lots of users opening and closing tabs), that retained set can hold meaningful RAM. Trim the count or period if your monitoring shows memory climbing during traffic spikes.
Cut per-circuit memory — the real scalability lever
Doubling your plan doubles capacity. Halving per-circuit memory also doubles capacity, for free. This is where the biggest wins hide:
Don't capture large objects in component fields. A field referencing a big list or DTO keeps it alive for the circuit's entire lifetime. Project to the minimal view model you actually render.
Virtualize long lists. Render only the rows on screen with <Virtualize> instead of materializing thousands of <tr> elements into the render tree.
Use @key on dynamic lists so the diffing algorithm reuses elements instead of rebuilding subtrees.
Implement IDisposable/IAsyncDisposable on components that subscribe to events or timers — an undisposed subscription is a per-circuit leak.
Set DetailedErrors = false in production (above): detailed errors retain extra diagnostic state per circuit.
Prerender static-ish pages so they don't open an interactive circuit at all (next section).
Virtualize is the single highest-leverage change for data-heavy apps:
<Virtualize Items="@_rows" Context="row" OverscanCount="4">
<tr>
<td>@row.Name</td>
<td>@row.Status</td>
</tr>
</Virtualize>
For database-bound grids, pair it with EF Core paging rather than loading everything — our EF Core performance patterns guide covers streaming and AsNoTracking queries that keep both RAM and CPU low.
Use render modes and prerendering (.NET 10)
.NET 10 keeps the unified render-mode model. The key scalability insight: a circuit only opens for interactive components. Pages that are mostly read-only — marketing pages, dashboards that render once, content views — can prerender on the server and never hold an interactive circuit, dramatically lowering your concurrent-circuit count.
// Program.cs — register interactive server components
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
@ Only THIS page opens a circuit — apply interactivity surgically @
@page "/dashboard"
@rendermode InteractiveServer
Apply @rendermode InteractiveServer per component or per page rather than globally. Prerendering also improves time-to-first-byte and SEO, because the initial HTML is fully rendered before the circuit attaches. If you're weighing the hosting model itself, our Blazor Server vs WebAssembly hosting comparison lays out the trade-offs, and the complete Blazor hosting guide covers all three modes.
Keep worker processes warm on IIS
A scalable app that cold-starts after every idle period feels broken. Blazor's first render after a worker recycle includes JIT and DI warm-up — 3–5 seconds of staring at a blank page. Fix it at the IIS level.
Set the app pool start mode to AlwaysRunning, disable the idle timeout, and enable IIS Application Initialization so a warm worker is ready before the first user arrives. On Adaptive Web Hosting you manage these through the Plesk panel's IIS settings — no server-admin access required.
Two more recycle-related settings worth auditing:
Scheduled recycling: the default fixed-interval recycle (every 1,740 minutes) drops all live circuits when it fires. Disable the fixed interval and schedule a specific off-peak time instead.
Memory-based recycling: set a private-memory limit slightly below your pool's RAM so a leak recycles gracefully instead of getting OOM-killed mid-request.
If your pool is recycling unexpectedly and taking circuits down with it, work through our app pool recycling diagnostic guide to find the trigger.
Tune the .NET 10 garbage collector
ASP.NET Core apps default to Server GC, which is what you want for many concurrent circuits — it uses multiple heaps and threads to keep allocation throughput high. The thing to verify is that nobody flipped it to Workstation GC, and that concurrent GC is on so collections don't pause every circuit at once:
<!-- .csproj -->
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>
On a memory-constrained 1 GB pool with very high circuit counts, Server GC's per-core heaps can occasionally use more memory than Workstation GC. It's worth measuring both under your real load if you're tight on RAM — but for the vast majority of apps, leave Server GC on. For a broader runtime comparison, see .NET 10 vs .NET 8 LTS performance.
Scaling out: when one pool isn't enough
Vertical scaling carries most apps a long way. If you genuinely outgrow a single 4 GB pool, the Blazor-specific catch is that a circuit lives on one server, so multi-instance setups need sticky sessions and a SignalR backplane (Azure SignalR Service or a Redis backplane) to coordinate.
Running one dedicated app pool — the Adaptive Web Hosting model — sidesteps the backplane problem entirely. There's no cross-server circuit coordination to get wrong because every circuit is on the same worker. Most Blazor Server apps never need to scale out; they need a right-sized pool and lean circuits.
Note that Adaptive Web Hosting plans don't include a managed Redis instance, so a backplane-based scale-out would point at an external Redis or Azure SignalR Service. For the apps this guide targets, you won't get there — vertical scaling plus the memory work above is the answer.
Monitor circuit count and memory
You can't tune what you don't measure. The three signals that predict a Blazor Server scaling problem are active circuit count, SignalR connection count, and app-pool private memory. .NET 10 exposes circuit and SignalR metrics through System.Diagnostics.Metrics, and you can watch worker-process memory in the Plesk monitoring panel.
Wire up structured logging and metrics so a slow memory climb shows up before it becomes a recycle. Our companion guide — .NET 10 monitoring and logging on IIS — walks through health checks, OpenTelemetry, and the exact counters to track. If CPU rather than memory is your bottleneck, diagnosing high CPU on .NET apps in IIS is the place to start.
For a worked example of Blazor Server carrying real concurrent load, see how we size an AI chatbot on Blazor Server and stream responses with SignalR streaming patterns.
Hosting recommendations
Match the plan to your concurrency target and component weight:
ASP.NET Business — $17.49/mo
Customer-facing apps with steady traffic. 2 GB pool plus higher-priority resource scheduling for more headroom. Most popular tier.
View Business plan →
ASP.NET Professional — $27.49/mo
High-concurrency or multi-app deployments. 4 GB pool with highest-priority CPU and resource scheduling for demanding interactive workloads.
View Professional plan →
FAQs
How many concurrent users can Blazor Server handle?
It depends almost entirely on per-circuit memory, not raw user count. A lean app at roughly 500 KB per circuit fits ~1,500 circuits in 1 GB and ~6,000 in 4 GB; a heavy app holding large state per component might manage a quarter of that. Measure your real circuit size, then divide your app-pool RAM by it.
Does Blazor Server need sticky sessions?
Yes when you run more than one server instance, because a circuit lives on one machine. On a single dedicated IIS Application Pool — the Adaptive Web Hosting default — there's only one worker, so sticky sessions and SignalR backplanes are unnecessary.
Why does my Blazor Server app use so much memory under load?
Usually large objects captured in component fields, undisposed event subscriptions, or non-virtualized lists — each multiplied across every circuit. Retained disconnected circuits also hold memory for a few minutes by default. Project to minimal view models, virtualize lists, dispose subscriptions, and trim DisconnectedCircuitRetentionPeriod.
Is Server GC or Workstation GC better for Blazor Server?
Server GC (the ASP.NET Core default) is best for high concurrency because it keeps allocation throughput high across multiple heaps. The only time to test Workstation GC is on a very small, memory-constrained pool with extreme circuit counts, where Server GC's per-core heaps can use more RAM.
Will prerendering reduce my concurrent circuit count?
Yes. Components that prerender and don't use @rendermode InteractiveServer never open an interactive circuit. Applying interactivity surgically — only to pages that truly need it — is one of the most effective ways to raise effective capacity.
Can I run Blazor Server on the Developer plan?
Absolutely. The 1 GB dedicated app pool on the Developer plan handles hundreds of concurrent circuits for typical apps. Move up to Business or Professional when your measured circuit count or component weight needs the extra RAM.
Bottom line
Blazor Server scales when you treat it as a memory budget: right-size the app pool, tune SignalR, keep workers warm, lean out your circuits, and watch the three signals that predict trouble. None of it requires Kubernetes or a backplane — it requires a dedicated app pool with guaranteed RAM and a little discipline in your components.
Adaptive Web Hosting's Blazor Server hosting is built for exactly this — Windows Server 2022, IIS 10, dedicated application pools, SQL Server 2022, and free SSL on every plan. Pick the tier that matches your concurrency target on the ASP.NET hosting plans page, or contact us if you want help sizing it.