Response Caching vs Output Caching in ASP.NET Core
ASP.NET Core has two caching mechanisms that sound similar but work differently: response caching and output caching. Response caching sets HTTP cache headers and relies on clients and proxies to cache responses. Output caching stores responses on the server and serves them directly without re-executing the endpoint. Understanding when to use each is key to effective caching.
Response Caching
Response caching is about HTTP cache headers. The ResponseCaching middleware reads Cache-Control headers and can serve cached responses, but it's primarily designed to tell downstream clients and CDNs how to cache.
Setup
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
Usage with Controllers
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
[ResponseCache(Duration = 60, VaryByQueryKeys = ["category"])]
public async Task<IActionResult> GetAll([FromQuery] string? category)
{
var products = await _repository.GetProductsAsync(category);
return Ok(products);
}
[HttpGet("{id}")]
[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any)]
public async Task<IActionResult> GetById(int id)
{
var product = await _repository.GetByIdAsync(id);
return product is not null ? Ok(product) : NotFound();
}
}
The [ResponseCache] attribute sets Cache-Control headers. Duration = 60 produces Cache-Control: public, max-age=60. The middleware can serve cached responses for subsequent requests, but it has significant limitations:
- It only caches GET and HEAD requests.
- It won't cache responses with
Set-Cookieheaders. - It won't cache responses for authenticated requests.
- It has no server-side cache invalidation — you can't purge a cached response.
Cache Profiles
Define reusable cache configurations:
builder.Services.AddControllers(options =>
{
options.CacheProfiles.Add("Default", new CacheProfile
{
Duration = 60,
Location = ResponseCacheLocation.Any
});
options.CacheProfiles.Add("NoCache", new CacheProfile
{
NoStore = true,
Location = ResponseCacheLocation.None
});
});
[ResponseCache(CacheProfileName = "Default")]
[HttpGet]
public IActionResult GetAll() => Ok(products);
Output Caching
Output caching, introduced in .NET 7, is a server-side cache. The server stores the complete response and serves it directly without executing the endpoint handler. This gives you full control over caching behaviour, including invalidation.
Setup
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromSeconds(30)));
options.AddPolicy("CacheByCategory", builder =>
builder.SetVaryByQuery("category")
.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("LongCache", builder =>
builder.Expire(TimeSpan.FromHours(1)));
});
var app = builder.Build();
app.UseOutputCache();
Usage with Minimal APIs
app.MapGet("/api/products", async (AppDbContext db) =>
await db.Products.ToListAsync())
.CacheOutput("CacheByCategory");
app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product p ? Results.Ok(p) : Results.NotFound())
.CacheOutput("LongCache");
Usage with Controllers
[HttpGet]
[OutputCache(PolicyName = "CacheByCategory")]
public async Task<IActionResult> GetAll([FromQuery] string? category)
{
var products = await _repository.GetProductsAsync(category);
return Ok(products);
}
Cache Invalidation with Tags
Output caching supports tag-based invalidation — when data changes, evict all related cached responses:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("Products", builder =>
builder.Tag("products")
.Expire(TimeSpan.FromMinutes(10)));
});
app.MapGet("/api/products", async (AppDbContext db) =>
await db.Products.ToListAsync())
.CacheOutput("Products");
app.MapPost("/api/products", async (
CreateProductRequest request,
AppDbContext db,
IOutputCacheStore cacheStore) =>
{
var product = new Product { Name = request.Name, Price = request.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
// Evict all cached responses tagged with "products"
await cacheStore.EvictByTagAsync("products", default);
return Results.Created($"/api/products/{product.Id}", product);
});
Custom Output Cache Policies
For complex logic, implement IOutputCachePolicy:
public class AuthenticatedUserCachePolicy : IOutputCachePolicy
{
public ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
var user = context.HttpContext.User;
if (user.Identity?.IsAuthenticated == true)
{
// Vary by user ID so each user gets their own cached response
context.Tags.Add($"user-{user.FindFirst(ClaimTypes.NameIdentifier)?.Value}");
context.EnableOutputCaching = true;
context.AllowCacheLookup = true;
context.AllowCacheStorage = true;
}
return ValueTask.CompletedTask;
}
public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
context.AllowCacheStorage = true;
return ValueTask.CompletedTask;
}
}
When to Use Which
| Scenario | Response Caching | Output Caching |
|---|---|---|
| CDN/browser caching | Yes | No |
| Server-side caching | Limited | Yes |
| Cache invalidation | No | Yes (tags) |
| Authenticated responses | No | Yes (with custom policy) |
| Distributed cache support | No | Yes (Redis) |
| Cache vary by header/query | Yes | Yes |
Use response caching when you want clients and CDNs to cache responses. It's about HTTP semantics.
Use output caching when you want the server to avoid re-executing expensive endpoint logic. It's about server performance.
You can use both together — output caching for server-side performance and response caching headers for downstream caching:
app.MapGet("/api/products", GetProducts)
.CacheOutput("Products");
// The output cache policy can also set Cache-Control headers
// so downstream caches benefit too
Key Points
- Response caching is HTTP header-based — it tells clients and proxies how to cache.
- Output caching is server-side — it stores and serves responses without running endpoint code.
- Output caching supports tag-based invalidation, making it practical for dynamic data.
- Use response caching for public, static-ish content that benefits from CDN caching.
- Use output caching for expensive queries where you control when the cache should be cleared.
Caching is one of the highest-leverage performance optimisations. Choosing the right mechanism ensures you get the benefits without serving stale data to your users.