Skip to content

Output & Response Caching

Key Points

  • Output caching (.NET 7+) caches server-side response bytes, keyed on configurable inputs (path, query, headers, etc.). Configurable per endpoint with policies.
  • Response caching middleware writes Cache-Control headers and short-circuits if a downstream cache (CDN, browser) might serve the response.
  • They're complementary: output caching for server speed, response caching for downstream cache hints.
  • OutputCacheStore can be in-memory or distributed (Redis). Output cache is shared across requests; response cache is per-instance.
  • Avoid caching authenticated/per-user responses without per-user cache keys.

Concepts (deep dive)

Output caching

builder.Services.AddOutputCache(o =>
{
    o.AddBasePolicy(b => b.Cache());                       // default: cache GET/HEAD with no auth
    o.AddPolicy("ByQueryAndUser", b => b
        .SetVaryByQuery("category", "page")
        .SetVaryByHeader("X-Tenant-Id")
        .Expire(TimeSpan.FromMinutes(5)));
});

app.UseOutputCache();

app.MapGet("/products", () => /* ... */).CacheOutput("ByQueryAndUser");
app.MapGet("/popular", () => /* ... */).CacheOutput(b => b.Expire(TimeSpan.FromMinutes(1)));

Output cache: - Stores the response body + headers server-side. - Keys on URL + configured varies (query, headers). - Default policy: skip authenticated requests, cache only GET/HEAD. - Can be evicted by tag (b.Tag("products")) and OutputCacheStore.EvictByTagAsync("products").

Distributed output cache

builder.Services.AddOutputCache();
builder.Services.AddStackExchangeRedisOutputCache(o => o.Configuration = "redis:6379");

Out-of-the-box Redis-backed cache. All replicas share state.

Response caching middleware

builder.Services.AddResponseCaching();
app.UseResponseCaching();

app.MapGet("/static-data", (HttpContext ctx) =>
{
    ctx.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
    {
        Public = true,
        MaxAge = TimeSpan.FromMinutes(10)
    };
    ctx.Response.Headers.Vary = "Accept-Encoding";
    return Results.Ok(/* ... */);
});

Sets Cache-Control: public, max-age=600 so CDNs and browsers can cache. The middleware also caches in-memory locally — a small win, but the bigger leverage is downstream.

💡 For static-ish data behind CDN, response caching is enough. For dynamic-ish data shared across users, output caching wins.

Tag-based eviction

app.MapGet("/products", () => /* ... */).CacheOutput(b => b.Tag("products"));
app.MapPost("/products", async (Product p, IOutputCacheStore store) =>
{
    /* save */
    await store.EvictByTagAsync("products", default);
    return Results.Created();
});

When data changes, evict the tag → all entries with that tag are invalidated.

VaryByValue for custom keys

o.AddPolicy("PerUser", b =>
    b.VaryByValue(ctx => ("user", ctx.User.Identity?.Name ?? "anon")));

Keys responses by computed value — works for tenants, locales, feature flags.

SetCacheKeyPrefix

If multiple apps share the same Redis output cache:

o.AddBasePolicy(b => b.SetCacheKeyPrefix("myapp:"));

Avoids collisions.

Common cache-control directives

Directive Meaning
public Any cache may store
private Only browser may store
no-cache Cache may store, but must revalidate
no-store No caching anywhere
max-age=N Cache for N seconds
s-maxage=N Same, but only for shared caches (CDN)
must-revalidate If stale, must validate before serving
immutable Resource never changes (long-cache assets)

For static fingerprinted assets (CSS/JS with hash in filename): Cache-Control: public, max-age=31536000, immutable.

Don't cache mistakes

app.MapGet("/account", (HttpContext ctx) =>
{
    var name = ctx.User.Identity?.Name;   // per-user
    return Results.Ok(name);
}).CacheOutput();   // ❌ default policy doesn't cache auth requests but be explicit

Default Cache() policy skips responses with auth cookies. Verify your policy and test with auth.

When response caching middleware skips

The middleware skips caching when: - Response is Set-Cookie-decorated (per-user state). - Authorization header present. - Cache-Control: no-store or private. - Method other than GET/HEAD. - Response status not "cacheable" by HTTP semantics (most non-2xx).

Cached entry size limits

builder.Services.AddOutputCache(o =>
{
    o.SizeLimit = 100 * 1024 * 1024;        // 100 MB total cache
    o.MaximumBodySize = 1 * 1024 * 1024;     // 1 MB per entry
});

In-memory output cache enforces these. Important to bound memory under heavy traffic.


Code: correct vs wrong

❌ Wrong: caching per-user data with no key

app.MapGet("/account", AccountHandler).CacheOutput();   // every user sees the first cached response

✅ Correct: vary by user

.CacheOutput(b => b.VaryByValue(ctx => ("user", ctx.User.Identity?.Name ?? "anon")));

(Better: don't cache per-user dynamic data; or use a short TTL.)

❌ Wrong: forgetting tag-based eviction

app.MapGet("/products", h).CacheOutput(b => b.Expire(TimeSpan.FromMinutes(60)));
// New product added; users see stale list for up to an hour.

✅ Correct

.CacheOutput(b => b.Tag("products").Expire(TimeSpan.FromMinutes(60)));
// On mutate: store.EvictByTagAsync("products", ct);

❌ Wrong: long max-age on dynamic data

ctx.Response.Headers.CacheControl = "public, max-age=86400";   // 24h on data that updates hourly

✅ Correct

ctx.Response.Headers.CacheControl = "public, max-age=300, s-maxage=900, stale-while-revalidate=600";

Design patterns for this topic

Pattern 1 — "Output cache for shared dynamic data"

  • Intent: server-side cache keyed by query/header.

Pattern 2 — "Tag-based eviction on writes"

  • Intent: invalidate related entries together.

Pattern 3 — "Response caching for downstream CDN"

  • Intent: offload to CDN/browser.

Pattern 4 — "Distributed output cache for multi-replica apps"

  • Intent: all replicas share cache state.

Pros & cons / trade-offs

Approach Pros Cons
Output cache (in-memory) Fast Per-instance
Output cache (Redis) Shared Network hop
Response caching CDN-friendly Limited to GET/HEAD
Per-action CacheOutput Explicit Sprinkled
Global policy DRY Harder to override

When to use / when to avoid

  • Use output caching for shared dynamic data (lists, popular items).
  • Use response caching for static-ish data behind CDN.
  • Avoid caching authenticated/per-user data without per-user keys.
  • Avoid long max-age on data that changes frequently.

Interview Q&A

Q1. Difference between output caching and response caching? Output caches the response server-side; response caching adds Cache-Control headers for downstream caches (CDN, browser).

Q2. When does the default output cache policy NOT cache? For non-GET/HEAD methods or requests with auth headers (cookies, Authorization).

Q3. How do you invalidate a cached entry? Tag-based: b.Tag("products") then OutputCacheStore.EvictByTagAsync("products", ct).

Q4. How do you cache differently per query string? b.SetVaryByQuery("category", "page") — cache key includes those query values.

Q5. Distributed output cache provider? AddStackExchangeRedisOutputCache() for Redis-backed storage shared across replicas.

Q6. What does s-maxage do? Cache directive interpreted only by shared caches (CDN). Lets you have different TTLs for browser vs CDN.

Q7. What's stale-while-revalidate? Tells caches: serve stale up to N seconds while revalidating in background. Smooths over backend hiccups.

Q8. Can you cache POST responses? Output caching: yes, via custom policy. Response caching middleware: no (HTTP semantics: POST is non-cacheable by default).

Q9. What's immutable cache-control directive for? Tells browsers the resource never changes — combined with hashed filenames for ultra-long caching.

Q10. Why might output cache not work behind a load balancer? In-memory output cache is per-instance. Use Redis-backed for shared cache.


Gotchas / common mistakes

  • ⚠️ Caching per-user data globally — wrong content served.
  • ⚠️ Long TTL with no eviction — stale data forever.
  • ⚠️ Authorization header silently skips caching.
  • ⚠️ Cache-Control: max-age=0 in response caching — middleware won't cache.
  • ⚠️ Forgetting MaximumBodySize — large responses bypass cache silently.

Further reading