Output Caching in ASP.NET Core
Output caching stores the complete HTTP response so that subsequent requests for the same resource are served directly from the cache, skipping the entire request pipeline. Introduced in .NET 7, this server-side caching middleware is a significant performance tool for read-heavy APIs and pages.
Basic Setup
Add the output caching services and middleware:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
var app = builder.Build();
app.UseOutputCache();
app.MapGet("/products", () =>
{
// This only runs on cache misses
return Results.Ok(GetProducts());
}).CacheOutput();
app.Run();
The CacheOutput() call marks the endpoint for caching. By default, the response is cached for subsequent requests with the same path and query string.
Cache Duration and Policies
Control cache behaviour with policies:
builder.Services.AddOutputCache(options =>
{
// Default policy — cache for 60 seconds
options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(60);
// Named policies
options.AddPolicy("ShortCache", policy =>
policy.Expire(TimeSpan.FromSeconds(10)));
options.AddPolicy("LongCache", policy =>
policy.Expire(TimeSpan.FromMinutes(30)));
options.AddPolicy("VaryByQuery", policy =>
policy.SetVaryByQuery("page", "pageSize", "sort"));
});
Apply named policies to endpoints:
app.MapGet("/products", GetProducts)
.CacheOutput("LongCache");
app.MapGet("/products/search", SearchProducts)
.CacheOutput("VaryByQuery");
app.MapGet("/dashboard/stats", GetStats)
.CacheOutput("ShortCache");
Varying the Cache
By default, responses are cached by path and query string. You can vary the cache by additional dimensions:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ByUser", policy =>
policy.SetVaryByHeader("Authorization"));
options.AddPolicy("ByCulture", policy =>
policy.SetVaryByHeader("Accept-Language")
.SetVaryByQuery("culture"));
options.AddPolicy("ByRouteValue", policy =>
policy.SetVaryByRouteValue("id"));
});
This means a cached response for /products?page=1 is stored separately from /products?page=2, and authenticated users with different tokens get separate cached responses.
Cache Tag Eviction
One of the most powerful features is tag-based cache invalidation. Tag your cached entries and evict them when the underlying data changes:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ProductCache", policy =>
policy.Tag("products")
.Expire(TimeSpan.FromMinutes(10)));
});
app.MapGet("/products", GetProducts)
.CacheOutput("ProductCache");
app.MapGet("/products/{id}", (int id) => GetProduct(id))
.CacheOutput(policy => policy
.Tag("products")
.SetVaryByRouteValue("id"));
// When a product is updated, evict all product caches
app.MapPut("/products/{id}", async (
int id,
UpdateProductRequest request,
IOutputCacheStore store,
CancellationToken ct) =>
{
await UpdateProduct(id, request);
await store.EvictByTagAsync("products", ct);
return Results.NoContent();
});
When you call EvictByTagAsync("products"), every cached response tagged with "products" is invalidated immediately.
Custom Cache Policies
For more complex scenarios, implement IOutputCachePolicy:
public class AuthenticatedUserCachePolicy : IOutputCachePolicy
{
public ValueTask CacheRequestAsync(
OutputCacheContext context, CancellationToken ct)
{
var attemptCache = context.HttpContext.User.Identity?.IsAuthenticated == true;
context.EnableOutputCaching = attemptCache;
context.AllowCacheLookup = attemptCache;
context.AllowCacheStorage = attemptCache;
context.AllowLocking = true;
context.CacheVaryByRules.HeaderNames =
new StringValues("Authorization");
return ValueTask.CompletedTask;
}
public ValueTask ServeFromCacheAsync(
OutputCacheContext context, CancellationToken ct)
{
return ValueTask.CompletedTask;
}
public ValueTask ServeResponseAsync(
OutputCacheContext context, CancellationToken ct)
{
context.Tags.Add("user-specific");
return ValueTask.CompletedTask;
}
}
Register it:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("AuthOnly", builder =>
builder.AddPolicy<AuthenticatedUserCachePolicy>());
});
Output Caching vs Response Caching
Do not confuse output caching with response caching. Response caching (UseResponseCaching) relies on HTTP cache headers and primarily works with downstream caches (CDNs, browsers). Output caching is entirely server-side — it stores responses in memory (or a distributed store) and serves them without involving HTTP cache semantics.
| Feature | Output Caching | Response Caching |
|---|---|---|
| Cache location | Server | Client/CDN/Server |
| Tag eviction | Yes | No |
| Vary support | Rich | HTTP headers only |
| Works with auth | Yes | No (by default) |
Key Takeaways
Output caching is the go-to choice for server-side response caching in ASP.NET Core. Define named policies for different cache durations, use tags for precise invalidation, and vary the cache by query strings, headers, or route values as needed. For read-heavy endpoints, the performance improvement can be dramatic — turning database-heavy responses into instant cache hits.