Performance and Caching
|
This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14. This section’s bibliography lists the reference material consulted while preparing these pages. |
Most ASP.NET Core performance work is caching what is expensive to recompute, limiting abusive load, and being async end to end. See Overview of performance best practices.
Response caching vs. output caching
| Where the copy lives | |
|---|---|
Response caching ( |
The client and intermediary proxies, driven
by HTTP |
Output caching ( |
On your server, in memory or a distributed store. You control policies, tags, and eviction. |
builder.Services.AddOutputCache(o =>
{
o.AddBasePolicy(b => b.Expire(TimeSpan.FromSeconds(30)));
o.AddPolicy("ByTenant", b => b.SetVaryByQuery("tenant").Tag("catalog").Expire(TimeSpan.FromMinutes(5)));
});
app.UseOutputCache();
app.MapGet("/catalog", GetCatalog).CacheOutput("ByTenant");
// invalidate on write
app.MapPost("/catalog", async (IOutputCacheStore cache, CancellationToken ct) =>
{
await cache.EvictByTagAsync("catalog", ct);
return Results.Ok();
});
Output caching is auth-aware (it will not serve one user’s response to another). The <cache> tag helper caches
view fragments. See
Output caching middleware.
In-process, distributed, and hybrid caches
// IMemoryCache -- single server
var products = await memoryCache.GetOrCreateAsync("products", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2);
entry.Size = 1;
return LoadProductsAsync();
});
// IDistributedCache -- shared across servers (Redis / SQL Server)
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = redisConnString);
// HybridCache (.NET 9+) -- L1 in-memory + L2 distributed, stampede protection, tag invalidation
builder.Services.AddHybridCache();
var value = await hybridCache.GetOrCreateAsync($"product:{id}", ct => LoadAsync(id, ct),
tags: ["product"]);
Rate limiting
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.AddFixedWindowLimiter("api", opt => { opt.Window = TimeSpan.FromSeconds(10); opt.PermitLimit = 100; });
o.AddPolicy("per-user", ctx => RateLimitPartition.GetTokenBucketLimiter(
ctx.User.Identity?.Name ?? "anon",
_ => new TokenBucketRateLimiterOptions { TokenLimit = 50, TokensPerPeriod = 10,
ReplenishmentPeriod = TimeSpan.FromSeconds(1) }));
o.OnRejected = (ctx, _) => { ctx.HttpContext.Response.Headers.RetryAfter = "10"; return ValueTask.CompletedTask; };
});
app.UseRateLimiter();
app.MapGet("/search", Search).RequireRateLimiting("api");
Limiter algorithms: fixed window, sliding window, token bucket, and concurrency. See Rate limiting middleware.
Timeouts, compression, and pooling
builder.Services.AddRequestTimeouts(o => o.DefaultPolicy = new() { Timeout = TimeSpan.FromSeconds(30) });
app.UseRequestTimeouts();
app.MapGet("/report", SlowReport).WithRequestTimeout(TimeSpan.FromMinutes(2));
builder.Services.AddResponseCompression(o => o.EnableForHttps = true);
app.UseResponseCompression();
Reuse buffers with ObjectPool<T>, ArrayPool<T>.Shared, and RecyclableMemoryStreamManager to cut GC
pressure on hot paths.
Resilience for outbound calls
builder.Services.AddHttpClient<IPricingClient, PricingClient>(c => c.BaseAddress = new Uri("https://pricing"))
.AddStandardResilienceHandler(); // Microsoft.Extensions.Http.Resilience: retry + circuit breaker + timeout + hedging
IHttpClientFactory also solves socket exhaustion and handler lifetime. See
Make HTTP requests using
IHttpClientFactory.
EF Core and runtime checklist
-
EF Core:
AddDbContextPool,AsNoTrackingfor reads,AsSplitQueryfor wide `Include`s, compiled queries for hot queries, and watch for N+1 (log the SQL). -
Be async all the way — never block on
.Result/.Wait(). -
Server GC (
<ServerGarbageCollection>true</ServerGarbageCollection>) for throughput. -
Tune Kestrel limits for your workload.
-
ReadyToRun and trimming cut startup and size; Native AOT (Minimal APIs) removes JIT and slashes memory and cold-start.
Caching hit paths
entry fresh?"} OC -- yes --> SRV["Server returns cached response
(no app code runs)"] OC -- no --> APP["Endpoint executes, response stored + tagged"] APP --> RC{"Response cacheable?
Cache-Control: public, max-age"} RC -- yes --> PROXY["Client / proxy caches its own copy"] RC -- no --> CLIENT["Client receives, does not store"]