When an ASP.NET Core application receives the same request repeatedly, it may perform the same work every time. It might query a database, call another API, build a response, and serialize the result even when the underlying data has not changed.
Caching can reduce this repeated work. However, ASP.NET Core offers different caching mechanisms, and choosing the wrong one can lead to unnecessary database calls or unexpected responses.
Two options are Output Caching and Response Caching. Both can help improve response times, but they work differently and solve different problems.
Output Caching stores generated responses on the server and can serve them again without running the endpoint for every request. Response Caching uses HTTP caching rules to determine whether a response can be stored and reused by a client or an eligible intermediary cache.
In this article, we will compare both approaches, implement them in ASP.NET Core, and look at when each one makes sense in a real application.
What Is Output Caching in ASP.NET Core?
Output Caching is a server-side caching feature available in ASP.NET Core. It stores the output generated by an endpoint and reuses it for later requests that match the same cache policy.
For example, imagine an API that returns a list of product categories. The list changes only occasionally, but hundreds of clients request it every minute.
Without caching, the application may query the database and serialize the same list for every request. With Output Caching, the server can reuse a previously generated response until the cached entry expires or is invalidated.
Output Caching is configured through ASP.NET Core middleware and endpoint policies.
How Output Caching works
The basic process is straightforward:
- A client sends a request to the API.
- The Output Caching middleware checks whether a matching response is available.
- If a valid cached response exists, the middleware returns it.
- If no matching response exists, the request continues to the endpoint.
- The endpoint generates a response, and the caching system stores it when the configured policy allows caching.
The important benefit is that a cache hit can avoid executing the endpoint again.
Output Caching supports configurable policies, expiration, cache variation, and cache invalidation. It also provides controls for deciding which requests should be cached.
What Is Response Caching in ASP.NET Core?
Response Caching follows HTTP caching rules. It uses response headers such as Cache-Control, Vary, and validators to indicate whether a response can be stored and reused.
This approach is useful when the application wants browsers or eligible intermediary caches to reuse responses according to standard HTTP rules.
For example, a public endpoint might return information that does not change frequently. The API can include caching headers that tell a client how long it may reuse the response.
Unlike Output Caching, Response Caching does not automatically mean that the application server will retain the generated response and bypass the endpoint on the next request.
The Response Caching middleware follows HTTP caching semantics. It considers request and response headers, including directives that prevent caching.
How Response Caching works
A typical response-caching flow looks like this:
- The client sends a request.
- The middleware checks whether an eligible cached response matches the request.
- If a reusable response exists, it may return that response.
- Otherwise, the request continues to the endpoint.
- The endpoint returns a response with suitable HTTP caching headers.
- The middleware and any eligible client or intermediary caches apply the relevant HTTP caching rules.
Response Caching is most useful when standard HTTP cache behavior is part of the application's design.
Output Caching vs. Response Caching
The main difference is where caching decisions are made and how the cached response is reused.
| Feature | Output Caching | Response Caching |
|---|
| Main purpose | Cache generated responses on the server | Follow HTTP response-caching rules |
| Server-side response storage | Built into the feature | Middleware can cache eligible responses, subject to HTTP rules |
| Endpoint execution on a cache hit | Can be skipped | Can be skipped when the middleware has an eligible cached response |
| Cache policy | Configured through ASP.NET Core policies | Primarily governed by HTTP request and response headers |
| Client and intermediary caching | Not its primary purpose | Designed around HTTP caching semantics |
| Cache invalidation | Supports policy-based expiration and explicit eviction | Depends on HTTP caching behavior and the cache implementation |
| User-specific data | Requires careful cache variation and authorization handling | Must follow HTTP rules and avoid exposing private responses |
| Typical use case | Repeated requests for public or carefully partitioned API responses | Responses intended to be cached according to HTTP rules |
Neither approach should be enabled blindly for every endpoint. The correct choice depends on the data, how often it changes, and whether the response may safely be reused.
Step 1: Configure Output Caching
To use Output Caching, register the service and add the middleware to the request pipeline.
In Program.cs, configure the following:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ProductCategories", policy =>
{
policy.Expire(TimeSpan.FromMinutes(5));
});
});
var app = builder.Build();
app.UseRouting();
app.UseOutputCache();
app.UseAuthorization();
app.MapControllers();
app.Run();
The AddOutputCache() method registers the output-caching services and defines a named policy. In this example, responses using the ProductCategories policy can be cached for five minutes.
The UseOutputCache() middleware must be placed in the request pipeline so it can inspect eligible requests and reuse stored responses. When using authorization, follow the documented middleware ordering for your application and ensure that private responses cannot be shared between users.
Now apply the policy to an endpoint:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/categories")]
public class CategoriesController : ControllerBase
{
private readonly AppDbContext _dbContext;
public CategoriesController(AppDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpGet]
[OutputCache(PolicyName = "ProductCategories")]
public async Task<IActionResult> GetCategories(
CancellationToken cancellationToken)
{
var categories = await _dbContext.Categories
.OrderBy(category => category.Name)
.Select(category => new
{
category.Id,
category.Name
})
.ToListAsync(cancellationToken);
return Ok(categories);
}
}
The endpoint retrieves the category list from the database and returns only the fields required by the API.
When a matching request hits the output cache, the middleware can return the cached response without running the database query again.
The five-minute expiration is an example, not a universal recommendation. If category information changes frequently, a shorter duration or explicit invalidation may be more suitable.
Step 2: Configure Response Caching
Response Caching requires its own service registration and middleware.
Add the following configuration to Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseRouting();
app.UseResponseCaching();
app.UseAuthorization();
app.MapControllers();
app.Run();
The AddResponseCaching() method registers the middleware's services. The UseResponseCaching() call enables response-cache processing in the pipeline.
Next, configure an endpoint to return an HTTP cache policy:
[HttpGet("public-info")]
public IActionResult GetPublicInfo()
{
Response.Headers.CacheControl = "public,max-age=60";
return Ok(new
{
Message = "This information is available to all users."
});
}
The Cache-Control header tells eligible caches that the response may be stored publicly for up to 60 seconds.
The word public is important. It indicates that the response may be stored by shared caches, so use it only when the content is safe to share among users.
The max-age=60 directive defines the freshness lifetime in seconds. After that period, a cache may need to fetch a new response.
Response Caching middleware does not guarantee that every response will be cached. The request and response must satisfy the applicable HTTP caching rules. For example, authorization, cookies, request directives, response directives, and other conditions can affect whether a response is eligible.
Step 3: Choose the Right Cache Policy for Your Data
Before caching an endpoint, ask three questions:
- How frequently does the data change?
- Can the same response be returned to different users?
- What should happen when the underlying data changes?
For public category lists, a short Output Caching policy may be sufficient. For static public information intended to be reused by browsers, HTTP Response Caching headers may be more appropriate.
For rapidly changing information, consider a shorter cache duration or a different caching strategy.
Example: Cache public data, not private user data
Suppose an API returns a list of available countries. Every user can receive the same list, and it changes infrequently. This is a reasonable candidate for server-side Output Caching.
Now consider an endpoint that returns the current user's account balance. Caching the response without correct user-specific isolation could expose one user's data to another user.
For sensitive responses, avoid shared caching unless the complete design guarantees correct isolation. In many cases, the simplest and safest approach is not to cache the response at all.
Step 4: Handle Data Changes and Cache Invalidation
Expiration prevents a cached response from being reused forever, but it does not immediately remove stale data when a record changes.
Suppose an administrator adds a new product category. If the category endpoint uses a five-minute cache duration, users may continue receiving the old list until the cache expires.
Output Caching supports explicit eviction through its caching services and tags. For example, you can tag related endpoints with a shared tag and invalidate those entries when the underlying data changes.
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("ProductCategories", policy =>
{
policy.Expire(TimeSpan.FromMinutes(5))
.Tag("category-list");
});
});
After a successful category update, an application service can evict the relevant tagged entries:
public class CategoryCacheService
{
private readonly IOutputCacheStore _cacheStore;
public CategoryCacheService(IOutputCacheStore cacheStore)
{
_cacheStore = cacheStore;
}
public ValueTask InvalidateCategoriesAsync(
CancellationToken cancellationToken = default)
{
return _cacheStore.EvictByTagAsync(
"category-list",
cancellationToken);
}
}
The application should call this service after the database update succeeds.
This example illustrates tag-based invalidation. Make sure the relevant endpoints use the tag and that the application's storage configuration supports the expected behavior. In a multi-instance deployment, use an appropriate shared output-cache store if cache entries and invalidation must be coordinated across instances.
Response Caching follows HTTP caching rules, so invalidation often depends on freshness lifetimes, validators, and the behavior of clients or intermediary caches. It does not provide the same endpoint-tag eviction workflow as Output Caching.
Common Mistakes to Avoid
| Mistake | Why it causes problems | Better approach |
|---|
| Caching every endpoint | Some data is private or changes frequently | Cache only suitable responses |
Using public for private data | Shared caches may expose information to other users | Use appropriate private or no-store policies |
| Setting a long expiration without considering updates | Users may receive stale data | Use shorter expiration or explicit invalidation |
| Assuming Response Caching always caches a response | HTTP rules may make a response ineligible | Inspect request and response headers |
| Ignoring authorization behavior | Cached output may bypass work that should run for each request | Review policy behavior and prevent unsafe sharing |
| Using separate caches without a plan | Multiple cache layers can have different freshness rules | Define which layer owns freshness and invalidation |
Troubleshooting Caching Problems
The endpoint still runs on every request
Check whether the response is eligible for caching, whether the middleware is registered correctly, and whether the requests match the same cache key. Also check for request headers or endpoint settings that prevent caching.
Users receive outdated data
Review the cache expiration and invalidation strategy. If the endpoint changes frequently, a long time-to-live may be inappropriate.
One user receives another user's response
Disable shared caching for the affected endpoint while investigating. Review cache variation, authorization, cookies, and any user-specific data included in the response. Do not rely on assumptions about which requests will be treated as equivalent.
Response Caching does not store the response
Inspect Cache-Control, Vary, authorization-related behavior, request directives, and other HTTP caching requirements. A response that is intentionally marked no-store, for example, should not be treated as reusable cached content.
Advantages and Disadvantages
Output Caching
Advantages:
- Can avoid repeated endpoint execution.
- Supports server-side cache policies and expiration.
- Provides tag-based invalidation for related entries.
- Works well for repeated requests for stable data.
Disadvantages:
- Requires careful policy design.
- Can serve stale data until expiration or invalidation.
- Needs additional planning in multi-instance deployments.
- Can create security risks if private responses are cached incorrectly.
Response Caching
Advantages:
- Uses standard HTTP caching headers.
- Works with the HTTP caching model used by browsers and eligible intermediaries.
- Allows applications to control response freshness through HTTP directives.
- Can reduce repeated response generation when the middleware has a reusable cached response.
Disadvantages:
- Caching depends on HTTP eligibility rules.
- Does not provide the same tag-based invalidation model as Output Caching.
- Can be confusing when request headers or authorization affect cache eligibility.
- Requires careful use of public, private, and no-store directives.
Best Practices for ASP.NET Core Caching
- Cache only responses that can safely be reused.
- Choose expiration based on how frequently the underlying data changes.
- Keep private user data out of shared cache entries.
- Use Output Caching when server-side response reuse and explicit invalidation are important.
- Use Response Caching when HTTP caching behavior is the primary requirement.
- Review cache variation when query parameters or headers affect the response.
- Invalidate related entries after successful updates when stale data is unacceptable.
- Measure response times, database load, and cache-hit behavior before and after enabling caching.
- Test cache behavior with authenticated and unauthenticated requests.
- In a multi-instance application, decide whether a shared cache is required and configure it accordingly.
Summary
Output Caching and Response Caching both help ASP.NET Core applications avoid unnecessary work, but they serve different purposes.
Output Caching is a strong choice when the server should reuse generated responses according to application-defined policies. Response Caching is useful when the application wants to follow standard HTTP caching rules through response headers.
The decision should depend on the response's freshness requirements, whether it contains private data, and how changes should invalidate cached content. A carefully chosen caching policy can reduce repeated database queries and endpoint execution without sacrificing correctness or exposing user information.
Join the conversation! Your thoughts help the community grow.