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

Example.cs
builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseResponseCaching();

Usage with Controllers

ProductsController.cs
[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:

Cache Profiles

Define reusable cache configurations:

Example.cs
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
    });
});
Example.cs
[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

Example.cs
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

Example.cs
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

Example.cs
[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:

Example.cs
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("Products", builder =>
        builder.Tag("products")
               .Expire(TimeSpan.FromMinutes(10)));
});
Example.cs
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:

AuthenticatedUserCachePolicy.cs
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:

Example.cs
app.MapGet("/api/products", GetProducts)
    .CacheOutput("Products");

// The output cache policy can also set Cache-Control headers
// so downstream caches benefit too

Key Points

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.