Freshness is a product requirement
A stale article title is usually tolerable. A stale account suspension, inventory promise, or permission decision may not be. Applying one cache duration across those values turns a performance setting into an undocumented business policy.
For every cached representation, define an acceptable age, the consequence of stale data, the owner of invalidation, and the behavior when the source is unavailable. This article proposes a practical design based on versioned representations and bounded stale serving. It does not assume that a cache can provide database-level consistency on its own.
Keep HTTP storage rules distinct from application caching
HTTP no-cache requires successful validation before reuse; it does not prohibit storage. no-store prohibits storage under the directive's rules. private restricts shared caches, while Vary participates in selecting the appropriate representation. These distinctions matter when responses pass through browsers and CDNs. RFC 9111
An internal Redis cache does not automatically inherit those semantics. The application still decides what it stores, how it identifies the representation, and when it may serve it. Document the browser, CDN, framework, and application cache layers separately; disabling one does not disable all of them.
Model the stale-refill race
The ordinary cache-aside sequence contains a subtle window:
- Reader A misses the cache and reads database version 12.
- Writer B commits version 13 and deletes the cache key.
- Reader A finishes serialization and stores version 12 under the now-empty key.
- Future readers receive version 12 until expiry or another invalidation.
A second delayed deletion reduces the window probabilistically but does not prove correctness. Instead, consider immutable value keys containing the entity version. Readers of version 13 cannot be overwritten by a late fill for version 12.
The remaining challenge is discovering the current version. If that pointer is itself cached, it has its own staleness budget. For operations requiring strict current state, resolve the version from the authoritative database or bypass the cache. Be explicit about this cost rather than claiming versioned keys eliminate every stale read.
Define the representation key completely
entity: product_204
version: 13
workspace: workspace_7
locale: en-GB
currency: GBP
price_list: contract_4
representation_schema: 3
This illustrative key includes dimensions that can change the returned value. Omit currency and a price can be reused incorrectly. Omit contract pricing and one customer may see another customer's offer. Omit representation schema and a rolling deployment may deserialize an incompatible value.
Avoid putting raw credentials or sensitive personal data into keys. Keys appear in diagnostics and metrics. Hashing the full authorization token is also a poor substitute for an explicit permission model and can create uncontrolled cardinality.
Cache shared data only after determining which portions are genuinely shared. Recheck current authorization before returning a representation, especially after membership changes or revocation. A cached result cannot grant access by itself.
Coordinate refill work without making locks permanent
When a popular key expires, thousands of identical misses can overwhelm the database. A single-flight mechanism lets one request refill while others wait briefly or receive permitted stale data. Bound the wait and the refill lease; a crashed owner must not block the key forever.
Use an ownership token for lock release so an old worker cannot delete a replacement worker's lease. If multiple refill generations can overlap, ensure an older result cannot replace a newer one. Add expiry jitter to spread routine refreshes, but do not confuse randomized expiry with concurrency correctness.
| Data class | Candidate failure behavior | Disallowed shortcut |
|---|---|---|
| Public editorial content | Serve bounded stale content | Serve unbounded stale content indefinitely |
| Analytics preview | Show age and degraded status | Present stale data as live |
| Permission decision | Revalidate or fail closed | Extend revoked access for availability |
| Checkout price | Recompute at commitment | Charge from an old display cache |
Observe freshness as well as hit rate
A high hit rate can describe a very fast incorrect system. Record representation age, source version, refill duration, stale-serving count, and invalidation lag. Keep metric labels bounded; entity IDs belong in sampled diagnostic records, not unrestricted time-series dimensions.
Test the stale-refill sequence with controlled barriers. Expire a hot key under load, kill the lease holder, change permissions during a refill, and deploy two representation schemas simultaneously. Verify that fallback traffic cannot overwhelm the database when the cache is unavailable.
Start by caching expensive, read-heavy values with tolerable staleness. Expand only when the freshness contract, invalidation path, and degraded behavior are understood. The performance benefit should be measured alongside the consistency behavior the product actually needs.
