A read-heavy endpoint is hammering the database. How do you add caching safely?
Quick Answer
Confirm the data tolerates staleness and by how much. Pick the layer — output caching for whole responses, a distributed cache (Redis) for data shared across instances, in-memory only if one instance. Set a sensible TTL, invalidate on write (tag or key eviction), guard against a cache stampede, and measure the hit rate.
First question: how stale can this data be, and is it the same for everyone or per-user? That decides everything. Then the layer — output caching if I can cache the whole response and evict it by tag when the underlying data changes; Redis if the cached data is shared across multiple app instances; in-memory only if there's one instance. The cache key has to include everything that varies the result, like the tenant. The write path is responsible for invalidating the right keys — and if I can't invalidate cleanly, I use a short TTL and live with a small staleness window. I also protect against a stampede when a hot key expires by using a single-flight lock so only one request refills it. Then I check the hit rate and the database load to confirm it actually helped.
- Is it cacheable? How stale can it be — seconds, minutes? Is it the same for every user or per-user/per-tenant (that affects the key)?
- Layer:
- Output caching (.NET 7+) if you can cache the whole response and evict by tag on writes.
IDistributedCache/ Redis for cached data shared across app instances.IMemoryCacheonly for a single instance or truly static reference data.
- Key — include everything that varies the result (route, query, tenant, maybe user).
- Invalidation — the write path evicts the relevant keys/tags. If you can't invalidate reliably, use a short TTL and accept the staleness window.
- Stampede — when a hot key expires, many requests miss at once and all hit the DB. Use a lock /
GetOrCreateAsyncwith a single-flight, or stale-while-revalidate. - Verify — log hit/miss ratio and DB load before and after.