Caching API Responses: What to Cache and When to Invalidate
Design response caches around freshness, authorization, invalidation, and failure behavior rather than hit rate alone.
Caching API Responses: What to Cache and When to Invalidate
A cache can make an endpoint feel dramatically faster. It can also serve yesterday's permissions, show the wrong account's data, or hide a slow database until every cached entry expires together.
Before adding Redis or another caching layer, describe what the response means and how old it may be. Freshness is part of the feature, not an implementation detail to settle after the cache is live.
Start with a response that can tolerate staleness
A public list of article categories is a different candidate from an account balance. Both can be cached in some designs, but they need different consistency rules and failure behavior.
For each endpoint, answer three questions: how often does the underlying data change, what happens if a reader sees an older value, and how expensive is rebuilding the response?
Measure the uncached endpoint first. If the main delay is an avoidable query or an unnecessarily large payload, fix that before adding a layer that can conceal the problem.
Make the key describe the response
A key must distinguish every relevant input to the cached representation. That can include tenant, resource, locale, filters, pagination position, and schema version.
An example naming scheme might be:
api:v2:account:456:projects:status=active:page=1:locale=en
The exact spelling matters less than the boundary it preserves. Leaving the account out of a tenant-specific response key can turn a performance optimization into a data leak.
Authorize the request before returning a cached private response. A cached result is not evidence that the current caller still has access. Avoid caching credentials or whole request objects, and normalize filter inputs so that equivalent requests do not create unnecessary duplicate entries.
Use cache-aside deliberately
In a cache-aside design, the application checks the cache, loads the source on a miss, and stores the result for later requests. The database remains authoritative.
Microsoft's cache-aside pattern guide describes the pattern and its consistency tradeoffs.
The read path is conceptually simple:
authorize request
build key from trusted scope and normalized filters
read cached representation
if present: return it
otherwise: query source, serialize response, cache with TTL
Decide how missing records behave too. Briefly caching a not-found result can reduce repeated work, but that negative entry must not hide a newly created resource longer than the product can tolerate.
TTL is a freshness policy
A time-to-live limits how long an entry remains available without being refreshed. It does not guarantee that the entry stays correct until expiry.
For a shared public summary, a short interval of staleness might be fine. For access revocation, waiting for a long TTL might be unacceptable. Choose the duration based on the effect of old information rather than picking an arbitrary round number for every endpoint.
Redis supports expiry options on the SET command, allowing the value and its expiry to be written together. A small amount of randomized TTL variation can also help avoid many entries expiring simultaneously.
Invalidate after a successful write
For a project update, commit the database change before invalidating the affected cached responses. Remember related representations such as detail pages, filtered lists, and summaries.
Deleting one detail key is insufficient if a list still contains the old project name. Keep the dependency map small enough to understand. If invalidation becomes a large web of fragile rules, reconsider what you cache or introduce a versioned grouping strategy.
Cache-aside has races. A reader can load an old database value before a write commits, then repopulate the cache after invalidation. Where that matters, consider version checks, coordinated refresh, or another consistency design. A basic delete-on-write pattern is not a promise of strong consistency.
Plan for misses and outages
Prevent a popular expired key from launching hundreds of identical expensive queries. Coalesce refreshes or use an appropriate distributed coordination mechanism, with bounded waiting and a clear failure path.
Decide whether a cache outage should fall back to the source. For optional public response caching, that may be reasonable if the database can handle it. Do not reuse that assumption for a cache-backed authorization or rate-limiting control.
Track hit rate, source latency, refresh failures, and stale-data incidents together. Test invalidation and tenant isolation as carefully as the successful hit. A useful cache improves response time while preserving a freshness contract readers can trust.