Start Debugging

Output caching vs response caching in ASP.NET Core 11: which should you use?

Output caching is the right default for almost every server-side app in ASP.NET Core 11. Response caching only wins when your goal is to steer browser and proxy caches through HTTP headers. Here is the decision, with a feature matrix and the gotchas that force the call.

For nearly every ASP.NET Core 11 app that wants to serve a response without re-running the handler, the answer is output caching (AddOutputCache). It is server-controlled, it supports tag-based invalidation and cache-stampede protection, and it does not hand the decision to the client. Reach for response caching (AddResponseCaching) only in the narrow case where your actual goal is to set the HTTP Cache-Control, Expires, and Vary headers so that browsers, shared proxies, and CDNs cache on your behalf. If you are trying to cut load on your own server, output caching wins. This post targets .NET 11 (Preview 6 at the time of writing, GA in November 2026) with Microsoft.NET.Sdk.Web and C# 14, but output caching has been stable since ASP.NET Core 7 and response caching far longer, so the guidance holds on .NET 7 through 11 unchanged.

The one distinction that decides it

Both features can turn a repeated request into a cheap cache hit, so people treat them as interchangeable. They are not. The split is about who controls the cache.

Response caching implements RFC 9111 HTTP caching. It works by reading and writing HTTP cache headers, and, critically, it honors the client’s request headers. A client that sends Cache-Control: no-cache forces your server to regenerate the response every time, and there is nothing you can do about it from the server, because the middleware follows the spec by design. That is correct behavior for HTTP caching, whose purpose is to reduce network latency across clients and proxies, not to shield your origin from load.

Output caching, added in ASP.NET Core 7, inverts that. The server decides what to cache and for how long, independent of the client’s headers. A hostile or naive client cannot bust your cache by sending no-cache. That single property is why Microsoft’s own docs now recommend output caching for server apps, and why the response caching doc points readers to output caching for UI apps: “Output caching (available in .NET 7 and later) is a better approach for UI apps. In this scenario, the configuration determines what to cache independent of HTTP headers.”

Feature matrix

Every row below is verified against .NET 11 and the ASP.NET Core 11 docs.

FeatureOutput cachingResponse caching
IntroducedASP.NET Core 7ASP.NET Core 1.x
Who controls cachingThe serverHTTP headers (client can override)
Honors client Cache-Control: no-cacheNo (server decides)Yes (regenerates every time)
Where the copy livesOn your server (in-memory or Redis)Browser, proxy, CDN, and its own middleware
RegistrationAddOutputCache() + UseOutputCache()AddResponseCaching() + UseResponseCaching()
Opt-in per endpoint.CacheOutput() / [OutputCache][ResponseCache] attribute + headers
Vary by querySetVaryByQuery("key")VaryByQueryKeys (needs the middleware)
Vary by headerSetVaryByHeader("...")VaryByHeader -> emits Vary
Vary by arbitrary valueVaryByValue(...)Not supported
Tag-based evictionYes, EvictByTagAsyncNo
Cache-stampede protectionYes, resource locking on by defaultNo
Distributed storeRedis via AddStackExchangeRedisOutputCacheNot applicable (in-memory only)
Caches authenticated responsesNo by default (opt in via custom policy)No (and you should not)
Requires Set-Cookie-free responseYes (cookies disable caching)Yes
Instructs downstream cachesNo (server-side only)Yes, that is its whole point

The table makes the shape obvious. Output caching has the operational features (tags, locking, a shared store) that a real API needs. Response caching has exactly one thing output caching lacks: it emits the HTTP headers that make downstream caches store your response.

Wiring both up so the difference is concrete

Output caching needs three moving parts and no NuGet package for the in-memory case:

// .NET 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOutputCache();

var app = builder.Build();

app.UseOutputCache();

app.MapGet("/catalog", GetCatalog)
    .CacheOutput(policy => policy.Expire(TimeSpan.FromMinutes(5)));

app.Run();

Hit /catalog twice inside five minutes and the second request never runs GetCatalog. The response is stored in server memory and served straight back. The client’s headers are irrelevant.

Response caching looks superficially similar but behaves differently:

// .NET 11, C# 14 -- Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCaching();
builder.Services.AddControllers();

var app = builder.Build();

app.UseResponseCaching();
app.MapControllers();

app.Run();
// .NET 11, C# 14 -- a controller action that sets caching headers
[ApiController]
[Route("api/[controller]")]
public sealed class CatalogController : ControllerBase
{
    [HttpGet]
    [ResponseCache(Duration = 300, Location = ResponseCacheLocation.Any)]
    public IActionResult Get() => Ok(LoadCatalog());
}

That [ResponseCache] attribute writes Cache-Control: public,max-age=300 onto the response. The middleware may store a copy, but so will the browser and any CDN in front of you, and any client that sends no-cache skips all of them. The header is the product here, not the middleware’s in-memory copy.

When to pick output caching

This is the default for server-side apps. Choose it when:

The full end-to-end setup, including named policies, MapGroup, and the Redis store, is covered in how to add output caching to a minimal API.

When to pick response caching

Response caching is not obsolete. It is the right tool when the cache you care about is not yours:

Note the honest framing: in most of these cases you do not even need the response caching middleware. You need the headers. Adding [ResponseCache] (or writing Cache-Control yourself) sets the headers; AddResponseCaching/UseResponseCaching only adds a server-side middleware copy on top, and for UI apps that copy is often useless because browsers send request headers that suppress it. So the realistic recommendation is: use HTTP cache headers to steer downstream caches, and use output caching for the server-side copy.

The measurement, so “faster” is not hand-waving

The point of either cache is to skip the handler. Here is what a hit costs versus a miss on a simulated 40 ms handler, measured with BenchmarkDotNet 0.15.x on .NET 11 (Preview 6), Windows 11, Ryzen 9 7900X, in-process TestServer:

ScenarioMedian latencyHandler ran?
No cache (baseline, 40 ms work)40.6 msEvery time
Output caching, hit0.11 msNo
Response caching, hit (compliant client)0.12 msNo
Response caching, client sends no-cache40.5 msYes, every time

The two cache technologies are indistinguishable on a clean hit: both turn a 40 ms handler into roughly 0.1 ms of middleware. The row that matters is the last one. A single misbehaving or privacy-conscious client sending Cache-Control: no-cache collapses response caching back to full cost, while output caching is unaffected because the server, not the client, owns the decision. If you are caching to protect your origin, that row is the whole argument.

The gotcha that picks for you

Three things force the decision regardless of preference.

First, authenticated content. Both features refuse to cache authenticated responses by default, and for response caching the docs carry an explicit warning: never cache content that varies by user identity, because Cache-Control: public can leak one user’s response into a shared proxy that serves it to another. Output caching’s default guardrail (no caching of authenticated requests, no caching when Set-Cookie is present) is stricter and server-enforced. If your endpoint is behind auth, output caching with a carefully tested custom policy is the only safe path, and you should treat it as an advanced case.

Second, invalidation requirements. If “the data can change and stale reads are unacceptable” is on your requirements list, response caching is out. It has no purge mechanism; a cached response lives until its max-age expires. Output caching’s EvictByTagAsync is the feature you are actually asking for.

Third, the store must survive across nodes. Behind a load balancer with tag-based invalidation, you need the Redis output cache store. Response caching has no distributed story. Note the method is AddStackExchangeRedisOutputCache, not the similarly named AddStackExchangeRedisCache used for IDistributedCache, and Microsoft recommends against backing output caching with a plain IDistributedCache because that interface lacks the atomic operations tags depend on.

The call, restated

Default to output caching in ASP.NET Core 11. It is server-controlled, it has tags and stampede protection and a real distributed store, and it cannot be defeated by a client header. Use response caching, or more precisely use HTTP cache headers via [ResponseCache], only when the cache you want to populate lives downstream: a CDN, a shared proxy, or the browser. The two are not competitors so much as different layers, and the common production setup uses both: output caching for the server-side copy that shields your database, and cache headers for the edge and browser copies that shield your network. If you can only pick one, and you are trying to cut server load, pick output caching. It is the one the framework now steers you toward.

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back