ASP.NET Core provides several ways to improve web application performance, but not all caching mechanisms work the same way. Output caching and response caching both help reduce the work required to serve HTTP requests, yet they solve different problems.
Output caching stores the results generated by an application and can reuse them for later requests that match the cache policy. Response caching uses HTTP caching rules to determine whether a response can be reused by a client or an intermediary cache, such as a proxy.
Choosing the wrong approach can lead to unnecessary database queries, unexpected cache misses, or responses that do not match the intended caching behavior.
This article explains how output caching and response caching work in ASP.NET Core, how to configure both, and when each option makes sense.
What Is Response Caching in ASP.NET Core?
Response caching follows HTTP caching semantics. It uses response headers, request headers, and caching rules to determine whether a response can be stored and reused.
For example, an application may return a public list of product categories that changes infrequently. A response can include cache-control headers that allow a client or intermediary cache to reuse it for a specified period.
ASP.NET Core provides response caching middleware through AddResponseCaching() and UseResponseCaching().
Configure Response Caching
First, register the response caching services in Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
app.MapControllers();
app.Run();
The middleware must run before the endpoints whose responses it should cache. If other middleware changes the response, its position may also affect caching behavior.
Next, create an endpoint that returns a cacheable response:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/categories")]
public class CategoriesController : ControllerBase
{
[HttpGet]
[ResponseCache(
Duration = 60,
Location = ResponseCacheLocation.Any)]
public IActionResult GetCategories()
{
var categories = new[]
{
"Books",
"Electronics",
"Home"
};
return Ok(categories);
}
}
The ResponseCache attribute configures the response caching headers. With a duration of 60 seconds and a location of Any, the response can be marked for public caching according to the resulting HTTP headers.
However, configuring these headers does not mean ASP.NET Core's response caching middleware will automatically store every response in its own server-side cache. The middleware follows HTTP caching rules, and actual reuse may occur in a browser or intermediary cache. Its behavior depends on the request, response, and cache directives.
This distinction matters when measuring performance. If a browser reuses a cached response, the request might not reach the server at all. If the request reaches the server, the middleware may or may not be able to serve a cached response.
What Is Output Caching in ASP.NET Core?
Output caching is designed to cache responses generated by an ASP.NET Core application. It can serve a previously generated response for a matching request without executing the endpoint again.
This is useful when an endpoint performs expensive work, such as querying a database, aggregating information, or building a public page.
Output caching was introduced in ASP.NET Core 7. It supports configurable policies, expiration, cache variation, and server-side caching behavior.
Configure Output Caching
Register output caching and define a policy in Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ProductCatalog", policy =>
{
policy.Expire(TimeSpan.FromMinutes(1));
});
});
var app = builder.Build();
app.UseOutputCache();
app.MapGet("/api/products", () =>
{
var products = new[]
{
new { Id = 1, Name = "Keyboard" },
new { Id = 2, Name = "Mouse" },
new { Id = 3, Name = "Monitor" }
};
return Results.Ok(products);
})
.CacheOutput("ProductCatalog");
app.Run();
The AddOutputCache() method registers the output caching services and defines a policy named ProductCatalog. The UseOutputCache() middleware enables output caching in the request pipeline.
The endpoint uses CacheOutput() to apply the policy. Once a response has been cached, a matching request can reuse it until the entry expires or is otherwise invalidated.
This example uses a fixed product list to keep the code simple. In a real application, output caching can avoid repeating expensive endpoint work, including database queries, when requests match the cache policy.
For controller actions, you can use the [OutputCache] attribute instead:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
[OutputCache(PolicyName = "ProductCatalog")]
public IActionResult GetProducts()
{
var products = new[]
{
new { Id = 1, Name = "Keyboard" },
new { Id = 2, Name = "Mouse" },
new { Id = 3, Name = "Monitor" }
};
return Ok(products);
}
}
Use the controller version when your application already follows the MVC pattern. The important part is that the output caching middleware is registered and the policy is applied to the endpoint.
Key Differences Between Output Caching and Response Caching
Both approaches can reduce repeated work, but their caching models differ.
Feature | Response Caching | Output Caching |
|---|
Primary purpose | Follow HTTP caching rules | Reuse application-generated responses |
Typical cache location | Client or intermediary cache; middleware can also cache eligible responses | Server-side application cache by default |
Configuration | HTTP response caching headers and middleware | Explicit policies and endpoint configuration |
Endpoint execution | May be avoided when middleware serves a cached response | Avoided when a matching cached output is served |
Expiration | Controlled by HTTP caching directives | Controlled by output caching policies |
Cache variation | HTTP headers and request variation rules | Configurable variation by query string, headers, or other policy settings |
Best fit | HTTP-aware caching across clients and intermediaries | Reducing repeated endpoint processing |
Invalidation | Depends on HTTP cache behavior and directives | Supports application-controlled eviction and expiration |
Neither approach is universally better. The correct choice depends on where caching should occur, how the data changes, and whether the application needs direct control over cached output.
When Should You Use Response Caching?
Response caching is a good fit when you want HTTP clients and intermediary caches to make caching decisions according to standard cache-control rules.
Typical examples include:
Public content that can safely be reused by browsers or proxies.
Static or infrequently changing API responses.
Responses that benefit from shared HTTP caching infrastructure.
Applications that need to communicate cache behavior through HTTP headers.
For example, a public endpoint that returns a list of supported countries may be suitable for response caching if the data changes infrequently.
You should be careful with personalized responses. If a response contains account details, private information, or user-specific data, public caching can expose information to other users. Choose cache directives based on the sensitivity of the response.
Also remember that response caching headers do not guarantee that a browser, proxy, or other client will store the response. Caches can apply their own rules, and requests may include directives that prevent reuse.
When Should You Use Output Caching?
Output caching is useful when you want the application to reuse a response rather than repeatedly execute the endpoint.
Common examples include:
Product catalogs.
Public dashboards.
Read-heavy reporting endpoints.
Reference data that changes occasionally.
Expensive queries that produce the same result for many requests.
Consider an endpoint that loads product data from a database and performs additional calculations before returning JSON. If many users request the same result, output caching can reduce repeated database access and processing.
For example, you can configure a short expiration period:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ShortLivedCatalog", policy =>
{
policy.Expire(TimeSpan.FromSeconds(30));
});
});
Apply this policy to the appropriate endpoint using CacheOutput("ShortLivedCatalog") or [OutputCache(PolicyName = "ShortLivedCatalog")].
A shorter expiration can reduce the time that stale data remains available, but it may also lead to more cache misses. Choose the duration based on how frequently the data changes and how much delay users can tolerate.
Configure Cache Variation Correctly
A cache must distinguish requests that legitimately need different responses.
Suppose an endpoint accepts a category query parameter:
/api/products?category=books
/api/products?category=electronics
If the application caches both requests under the same cache key, one category could receive another category's response. The cache policy must vary by the relevant query parameter.
For output caching, you can configure query-string variation:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ProductsByCategory", policy =>
{
policy
.Expire(TimeSpan.FromMinutes(1))
.SetVaryByQuery("category");
});
});
Apply the policy to the endpoint that reads the category query parameter.
The same principle applies to language, tenant, headers, and other request values that affect the response. Configure variation only for values that genuinely change the output.
Do not assume that authentication automatically makes a response safe to cache. For personalized endpoints, carefully review the policy and ensure one user's response cannot be served to another user. Avoid caching sensitive output unless the design explicitly provides safe isolation and appropriate invalidation.
Handle Cache Invalidation and Stale Data
Expiration alone may not be enough when the application updates its data.
Imagine that an administrator changes a product's price. If the catalog response is cached for five minutes, users may continue seeing the old price until the entry expires.
Output caching supports tag-based eviction. You can assign a tag to related endpoints and remove cached entries associated with that tag when the underlying data changes.
For example:
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ProductCatalog", policy =>
{
policy
.Expire(TimeSpan.FromMinutes(5))
.Tag("product-catalog");
});
});
When the application updates the catalog, it can evict the tagged entries:
app.MapPost("/api/cache/products/clear", async (
IOutputCacheStore cacheStore) =>
{
await cacheStore.EvictByTagAsync(
"product-catalog",
CancellationToken.None);
return Results.NoContent();
});
The second endpoint is a simplified example of invalidation logic. In a production application, cache eviction should be triggered by the trusted business operation that updates the products, not exposed as an unrestricted public endpoint.
For applications running on multiple servers, confirm that the configured cache store and deployment architecture provide the invalidation behavior you require. An in-memory cache is local to an application instance, so invalidating one instance does not automatically clear unrelated instances.
Common Caching Mistakes
Caching data that changes frequently
A long expiration period can cause users to see stale data. Choose an appropriate lifetime and invalidate entries when important updates occur.
Caching personalized responses as public content
Responses containing user-specific or confidential information must not be shared across users. Review HTTP cache directives and output caching policies carefully.
Forgetting query-string variation
If a query parameter changes the result, make sure the cache key distinguishes its values. Otherwise, unrelated requests can receive the same cached response.
Assuming every request will be cached
A request may not be eligible for caching because of its method, headers, response status, authorization behavior, or policy configuration. Test the actual application rather than assuming that adding an attribute guarantees a cache hit.
Ignoring cache behavior across instances
An in-memory output cache is not automatically shared across a server farm. Decide whether local caching is sufficient or whether the application needs a distributed caching design.
How to Choose the Right Option
Use response caching when your primary goal is to communicate HTTP caching rules to browsers and intermediary caches.
Use output caching when your primary goal is to avoid repeating endpoint execution for matching requests.
Some applications benefit from both approaches, but combining them requires care. Verify the resulting HTTP headers, cache keys, expiration rules, and invalidation behavior. Adding multiple caching layers without understanding how they interact can make stale responses difficult to diagnose.
Measure performance before and after enabling caching. Track endpoint latency, database load, cache hit rates where available, and the freshness of returned data. These measurements help determine whether caching solves the actual bottleneck.
Conclusion
ASP.NET Core response caching and output caching address different needs. Response caching follows HTTP caching semantics and is useful when clients or intermediary caches should reuse eligible responses. Output caching allows the application to reuse generated responses and avoid repeating expensive endpoint work.
Choose the mechanism based on the location of the cache, the behavior of the endpoint, and the freshness requirements of the data. Configure cache variation correctly, protect personalized responses, and plan invalidation before enabling caching in production.
A well-designed caching policy can improve response times and reduce server load without introducing unexpected data inconsistencies.
Join the conversation! Your thoughts help the community grow.