Events
Power BI DataViz World Championships
Feb 14, 4 PM - Mar 31, 4 PM
With 4 chances to enter, you could win a conference package and make it to the LIVE Grand Finale in Las Vegas
Learn moreThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
This isn't the latest version of this article. For the current release, see the .NET 9 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 9 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 9 version of this article.
This article explains how to host and deploy server-side Blazor apps (Blazor Web Apps and Blazor Server apps) using ASP.NET Core.
Server-side Blazor apps can accept Generic Host configuration values.
Using a server-side hosting model, Blazor is executed on the server from within an ASP.NET Core app. UI updates, event handling, and JavaScript calls are handled over a SignalR connection.
A web server capable of hosting an ASP.NET Core app is required. Visual Studio includes a server-side app project template. For more information on Blazor project templates, see ASP.NET Core Blazor project structure.
Publish an app in Release configuration and deploy the contents of the bin/Release/{TARGET FRAMEWORK}/publish
folder, where the {TARGET FRAMEWORK}
placeholder is the target framework.
When considering the scalability of a single server (scale up), the memory available to an app is likely the first resource that the app exhausts as user demands increase. The available memory on the server affects the:
For guidance on building secure and scalable server-side Blazor apps, see the following resources:
Each circuit uses approximately 250 KB of memory for a minimal Hello World-style app. The size of a circuit depends on the app's code and the state maintenance requirements associated with each component. We recommend that you measure resource demands during development for your app and infrastructure, but the following baseline can be a starting point in planning your deployment target: If you expect your app to support 5,000 concurrent users, consider budgeting at least 1.3 GB of server memory to the app (or ~273 KB per user).
SignalR's hosting and scaling conditions apply to Blazor apps that use SignalR.
For more information on SignalR in Blazor apps, including configuration guidance, see ASP.NET Core Blazor SignalR guidance.
Blazor works best when using WebSockets as the SignalR transport due to lower latency, better reliability, and improved security. Long Polling is used by SignalR when WebSockets isn't available or when the app is explicitly configured to use Long Polling.
A console warning appears if Long Polling is utilized:
Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection.
Recommendations for global deployments to geographical data centers:
Hosting on Azure App Service requires configuration for WebSockets and session affinity, also called Application Request Routing (ARR) affinity.
Note
A Blazor app on Azure App Service doesn't require Azure SignalR Service.
Enable the following for the app's registration in Azure App Service:
The optional Azure SignalR Service works in conjunction with the app's SignalR hub for scaling up a server-side app to a large number of concurrent connections. In addition, the service's global reach and high-performance data centers significantly aid in reducing latency due to geography.
The service isn't required for Blazor apps hosted in Azure App Service or Azure Container Apps but can be helpful in other hosting environments:
The Azure SignalR Service with SDK v1.26.1 or later supports SignalR stateful reconnect (WithStatefulReconnect).
In the event that the app uses Long Polling or falls back to Long Polling instead of WebSockets, you may need to configure the maximum poll interval (MaxPollIntervalInSeconds
, default: 5 seconds, limit: 1-300 seconds), which defines the maximum poll interval allowed for Long Polling connections in the Azure SignalR Service. If the next poll request doesn't arrive within the maximum poll interval, the service closes the client connection.
For guidance on how to add the service as a dependency to a production deployment, see Publish an ASP.NET Core SignalR app to Azure App Service.
For more information, see:
For a deeper exploration of scaling server-side Blazor apps on the Azure Container Apps service, see Scaling ASP.NET Core Apps on Azure. The tutorial explains how to create and integrate the services required to host apps on Azure Container Apps. Basic steps are also provided in this section.
Configure Azure Container Apps service for session affinity by following the guidance in Session Affinity in Azure Container Apps (Azure documentation).
The ASP.NET Core Data Protection (DP) service must be configured to persist keys in a centralized location that all container instances can access. The keys can be stored in Azure Blob Storage and protected with Azure Key Vault. The DP service uses the keys to deserialize Razor components. To configure the DP service to use Azure Blob Storage and Azure Key Vault, reference the following NuGet packages:
Azure.Identity
: Provides classes to work with the Azure identity and access management services.Microsoft.Extensions.Azure
: Provides helpful extension methods to perform core Azure configurations.Azure.Extensions.AspNetCore.DataProtection.Blobs
: Allows storing ASP.NET Core Data Protection keys in Azure Blob Storage so that keys can be shared across several instances of a web app.Azure.Extensions.AspNetCore.DataProtection.Keys
: Enables protecting keys at rest using the Azure Key Vault Key Encryption/Wrapping feature.Note
For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.
Update Program.cs
with the following highlighted code:
using Azure.Identity;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Azure;
var builder = WebApplication.CreateBuilder(args);
var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];
builder.Services.AddRazorPages();
builder.Services.AddHttpClient();
builder.Services.AddServerSideBlazor();
builder.Services.AddAzureClientsCore();
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
new DefaultAzureCredential())
.ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
new DefaultAzureCredential());
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
The preceding changes allow the app to manage the DP service using a centralized, scalable architecture. DefaultAzureCredential discovers the container app managed identity after the code is deployed to Azure and uses it to connect to blob storage and the app's key vault.
To create the container app managed identity and grant it access to blob storage and a key vault, complete the following steps:
scalablerazorstorage
.Repeat the preceding settings for the key vault. Select the appropriate key vault service and key in the Basics tab.
When using IIS, enable:
For more information, see the guidance and external IIS resource cross-links in Publish an ASP.NET Core app to IIS.
Create an ingress definition with the following Kubernetes annotations for session affinity:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: <ingress-name>
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"
Follow the guidance for an ASP.NET Core SignalR app with the following changes:
location
path from /hubroute
(location /hubroute { ... }
) to the root path /
(location / { ... }
).proxy_buffering off;
) because the setting only applies to Server-Sent Events (SSE), which aren't relevant to Blazor app client-server interactions.For more information and configuration guidance, consult the following resources:
To host a Blazor app behind Apache on Linux, configure ProxyPass
for HTTP and WebSockets traffic.
In the following example:
ProxyPreserveHost On
ProxyPassMatch ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass /_blazor ws://localhost:5000/_blazor
ProxyPass / http://localhost:5000/
ProxyPassReverse / http://localhost:5000/
Enable the following modules:
a2enmod proxy
a2enmod proxy_wstunnel
Check the browser console for WebSockets errors. Example errors:
For more information and configuration guidance, consult the following resources:
JS interop can be used to measure network latency, as the following example demonstrates.
MeasureLatency.razor
:
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
@inject IJSRuntime JS
<h2>Measure Latency</h2>
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
@inject IJSRuntime JS
@if (latency is null)
{
<span>Calculating...</span>
}
else
{
<span>@(latency.Value.TotalMilliseconds)ms</span>
}
@code {
private DateTime startTime;
private TimeSpan? latency;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
startTime = DateTime.UtcNow;
var _ = await JS.InvokeAsync<string>("toString");
latency = DateTime.UtcNow - startTime;
StateHasChanged();
}
}
}
For a reasonable UI experience, we recommend a sustained UI latency of 250 ms or less.
On the server, a new circuit is created for each user session. Each user session corresponds to rendering a single document in the browser. For example, multiple tabs create multiple sessions.
Blazor maintains a constant connection to the browser, called a circuit, that initiated the session. Connections can be lost at any time for any of several reasons, such as when the user loses network connectivity or abruptly closes the browser. When a connection is lost, Blazor has a recovery mechanism that places a limited number of circuits in a "disconnected" pool, giving clients a limited amount of time to reconnect and re-establish the session (default: 3 minutes).
After that time, Blazor releases the circuit and discards the session. From that point on, the circuit is eligible for garbage collection (GC) and is claimed when a collection for the circuit's GC generation is triggered. One important aspect to understand is that circuits have a long lifetime, which means that most of the objects rooted by the circuit eventually reach Gen 2. As a result, you might not see those objects released until a Gen 2 collection happens.
Prerequisites:
We compute the memory used by blazor as follows:
(Active Circuits × Per-circuit Memory) + (Disconnected Circuits × Per-circuit Memory)
The amount of memory a circuit uses and the maximum potential active circuits that an app can maintain is largely dependent on how the app is written. The maximum number of possible active circuits is roughly described by:
Maximum Available Memory / Per-circuit Memory = Maximum Potential Active Circuits
For a memory leak to occur in Blazor, the following must be true:
In other cases, there's no memory leak. If the circuit is active (connected or disconnected), the circuit is still in use.
If a collection for the circuit's GC generation doesn't run, the memory isn't released because the garbage collector doesn't need to free the memory at that time.
If a collection for a GC generation runs and frees the circuit, you must validate the memory against the GC stats, not the process, as .NET might decide to keep the virtual memory active.
If the memory isn't freed, you must find a circuit that isn't either active or disconnected and that's rooted by another object in the framework. In any other case, the inability to free memory is an app issue in developer code.
Adopt any of the following strategies to reduce an app's memory usage:
When building a Blazor app that runs on the client and targets mobile device browsers, especially Safari on iOS, decreasing the maximum memory for the app with the MSBuild property EmccMaximumHeapSize
may be required. For more information, see Host and deploy ASP.NET Core Blazor WebAssembly.
dotnet-counters
. For more information see Investigate performance counters (dotnet-counters).dotnet-counters
because you'll see the GCs happen and the amount of used memory go down to 0 (zero), but you won't see the working set counter decrease, which is the sign that .NET is holding on to the memory to reuse it. For more information on project file (.csproj
) settings to control this behavior, see Runtime configuration options for garbage collection.GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true))
free the memory?ASP.NET Core feedback
ASP.NET Core is an open source project. Select a link to provide feedback:
Events
Power BI DataViz World Championships
Feb 14, 4 PM - Mar 31, 4 PM
With 4 chances to enter, you could win a conference package and make it to the LIVE Grand Finale in Las Vegas
Learn moreTraining
Learning path
Use advance techniques in canvas apps to perform custom updates and optimization - Training
Use advance techniques in canvas apps to perform custom updates and optimization
Documentation
Host and deploy ASP.NET Core Blazor
Discover how to host and deploy Blazor apps.
Host and deploy ASP.NET Core Blazor WebAssembly
Learn how to host and deploy Blazor WebAssembly using ASP.NET Core, Content Delivery Networks (CDN), file servers, and GitHub Pages.
Multiple hosted ASP.NET Core Blazor WebAssembly apps
Learn how to configure a hosted Blazor WebAssembly app to host multiple Blazor WebAssembly apps.