Modern .NET Development
Caching in ASP.NET Core: What to Cache and What Not to Cache
A cache trades freshness and operational simplicity for latency and capacity. The hard decision is not which API stores a value, but whether stale or incorrectly scoped data is an acceptable failure.

Cache only after defining the tolerated lie
Every cache can return data that is older than the source. Before adding one, state how stale the value may be, who can observe it, what event invalidates it and what happens when the cache is unavailable. If nobody can answer, the application is adopting an undefined consistency model.
Measure the bottleneck first. Caching a cheap query while one unindexed report dominates the database adds invalidation code without useful capacity. A cache hit ratio alone is not success; measure source load, latency, staleness and correctness incidents.
Choose a cache by the boundary it serves
| Cache | Best use | Primary trap |
|---|---|---|
| Browser/CDN HTTP cache | Public or safely vary-able representations | Leaking personalised responses |
| ASP.NET Core output cache | Whole endpoint responses | Incomplete variation by identity, tenant or query |
| In-memory cache | Hot local reference or computed data | Different values on each instance |
| Distributed cache | Shared derived values across instances | Network and serialisation become dependencies |
| Database materialisation | Expensive stable read models | Refresh and ownership complexity |
The closest cache to the consumer usually saves the most work, but it also has the least application context. Cache at the highest safe layer, not reflexively inside every repository.

Cache keys are an authorisation boundary
If tenant, user, locale, currency, permissions, feature cohort or query affects the result, the key or cache policy must account for it. A missing tenant dimension is a cross-tenant data leak with excellent performance.
Canonicalise keys. Equivalent filters with different query ordering should not create unrelated entries, while semantically different requests must not collide. Hash very large or sensitive key material and avoid placing tokens, emails or secrets in observable cache keys.
Authorisation can change before content expires. For highly sensitive data, cache only after an access decision that is re-evaluated on each request, or do not cache the personalised representation at all.
Memory cache is per process
IMemoryCache is fast and simple, but each application instance has independent entries and loses them on restart. It works for data whose inconsistency between instances is acceptable. It is not distributed coordination, a lock service or durable state.
Set size limits where unbounded key cardinality can grow. Sliding expiration can keep unpopular-but-periodically-touched data forever; absolute expiration provides an upper bound. Eviction callbacks are notifications, not reliable jobs, and may run for reasons other than expiry.
Never assume a cached object is immutable. Returning a mutable reference lets one request alter what another receives. Cache immutable values or copy at the boundary.
Distributed cache replaces CPU with network and serialization
Redis or another shared cache gives instances a common view, but every hit now has latency, failure and serialization cost. For tiny cheap database reads, it may be slower. Batch operations and local near-caches can help only with another layer of consistency decisions.
Version the serialized value or key namespace so deployments can read old entries safely. Rolling deployments mean old and new schemas coexist. A deserialization failure should fall back to source and replace the entry rather than repeatedly fail requests.
Decide whether cache failure fails open to the source or fails the request. Falling back is common, but a total cache outage can create a database stampede. Protect the source with concurrency limits and degraded behaviour.
Output caching needs a representation model
Output caching can avoid endpoint execution entirely. That makes variation and invalidation critical. Vary on every request property that changes the response, and do not cache authenticated or cookie-setting responses without a deliberate policy.
HTTP validators such as ETags can save transfer while still checking representation freshness. Public cache headers allow browsers and CDNs to serve without reaching the application. These may produce larger savings than an application object cache and preserve standard semantics.
Do not cache error responses accidentally. A transient 500 or one user's 404 can become a fleet-wide answer if intermediary rules are careless.
Invalidation is a data ownership problem
Time-to-live is not invalidation; it is a maximum stale period. It is enough for exchange rates, reference lists or content where bounded staleness is accepted. Account balances, revoked access and workflow state may need event-driven invalidation or no cache.
Delete-on-write has a race: a reader can fetch old source data, the writer commits and invalidates, then the reader stores the old value after invalidation. Versioned keys, source versions, write-through sequencing or very short lifetimes can close or bound the window.
Broad prefix deletion is often expensive and unsupported by cache stores without scanning. Design keys and invalidation indexes before millions of entries exist. Namespace versioning can invalidate a family by changing one generation value, at the cost of leaving old entries to expire.
Prevent stampedes without creating a global lock
When a popular entry expires, many requests may recompute it simultaneously. Use single-flight coordination per key, refresh-ahead or stale-while-revalidate. Add TTL jitter so related keys do not expire at once.
A process-local lock does not coordinate several instances. A distributed lock adds failure modes and may outlive its owner. Often the safest design is to serve a bounded stale value while one worker refreshes, protecting the source with a concurrency limit.
Negative caching can prevent repeated lookups for absent data, but use short lifetimes. A newly created record should not remain “missing” because a previous 404 was cached for an hour.
Some data should not be cached
- Secrets or tokens whose revocation must be immediate.
- Highly sensitive personalised responses without a proven isolation key.
- Rapidly changing values when stale decisions cause financial or safety harm.
- Cheap data where invalidation costs more than retrieval.
- Large unbounded objects that displace more valuable entries.
- State used as a durable workflow or distributed lock.
- Failures whose cause is transient or identity-specific.
Caching a permission result is especially risky because organisation membership changes asynchronously. If it is necessary, use short bounded lifetimes and a security-version or revocation mechanism for urgent change.
Observe correctness as well as hits
Measure hits, misses, load duration, entry age, refresh failure, eviction and fallback pressure by cache name-not raw key. Sample comparisons against source where the domain permits it. A 99% hit rate can conceal one catastrophic tenant-key collision.
Load-test cold start and cache outage. Warm-cache benchmarks describe the easiest state. Deployments, eviction and regional failover create cold periods when the source must absorb full load.
Production checklist
- Define acceptable staleness and cache-failure behaviour.
- Include tenant, identity and representation dimensions in keys.
- Bound key cardinality, value size and lifetime.
- Version values across rolling deployments.
- Protect the source during cache outage and expiry.
- Design invalidation races, not only happy-path deletion.
- Prevent stampedes with per-key coordination or stale refresh.
- Use HTTP caching where it safely avoids more work.
- Test cold caches, outages and cross-tenant isolation.
Related C3 Software guides
Frequently asked questions
Should ASP.NET Core use memory or distributed cache?
Use memory when per-instance values and loss on restart are acceptable. Use distributed cache when instances need a shared derived value and the extra dependency is justified.
How long should cache entries live?
Derive lifetime from tolerated staleness and source capacity, not a universal default.
How do I prevent cache stampedes?
Use per-key single-flight loading, TTL jitter, refresh-ahead or bounded stale-while-revalidate, and protect the source.
Can permissions be cached?
Only with a freshness and urgent-revocation design. A cached allow decision can outlive the access it represents.
Use caching without compromising correctness
C3 Software Limited has built and supported business software in the UK since 2001. If cache invalidation or tenant isolation is becoming risky, talk to C3 Software about an architecture review.