Performance

Choose caching only after defining what may become stale

Kamruz
Kamruz
Oct 14, 2026
5 min read
Choose caching only after defining what may become stale

A cache makes an application faster by reusing a result instead of repeating work. That sentence leaves out the difficult part: when is the reused result still acceptable? Without a freshness requirement, a cache can turn a slow correct response into a fast misleading one. The right starting point is the meaning of the data and the events that change it.

Consider an illustrative project catalogue. Public descriptions change infrequently, but an administrator must see a saved edit immediately. A customer-specific availability value changes much more often and must not be reused across accounts. These are not three settings for one universal cache. They are different contracts about freshness, identity and visibility.

Explain what stale means for this feature

Ask what a visitor might misunderstand if the value is old. An older public biography may be inconvenient. An outdated access decision can expose protected data. A stale price or availability value can create a promise the system cannot honor. Classify the consequence before choosing a time-to-live.

Write an explicit tolerance where the product can accept one. “Public project descriptions may be reused briefly after an edit” is more meaningful than “cache for performance.” For actions that require immediate correctness, identify the authoritative read at the decision point. A display can tolerate staleness even when the action it previews must revalidate.

Separate the reader's expectation from the maintainer's expectation. An administrator clicking Save often needs confirmation of the new value. That workflow may bypass a public cache or invalidate it after a successful write. Returning the old value with a success message creates ambiguity about whether the operation actually persisted.

Identify every cache in the request path

Trace the complete path from browser to data store. A browser cache, CDN, framework data cache, in-process map, and database-side behavior can all affect the response. Updating one layer does not guarantee the next request observes fresh data. Record where the value is stored and which key identifies it at each layer.

Inspect actual response headers and deployment behavior. HTTP caching rules distinguish private and shared reuse, but the header alone may not describe a separate application-level cache. Likewise, a framework setting does not explain a custom module-level map. For confusing behavior, trace the specific URL and query rather than assuming the whole application follows one policy.

Use a direct server data path when self-fetching introduces unnecessary layers. Fewer layers do not automatically make the system faster, but they can make ownership and invalidation easier to reason about. Avoid removing a layer whose operational role you have not understood.

Design the key from the result's identity

A cache key should represent all inputs that can change the visible result. These may include resource ID, locale, pagination cursor, filter, tenant, and authorization context. Missing an important dimension can produce the wrong result even when the entry is perfectly fresh. Time-to-live cannot fix a key that mixes two users' data.

Be cautious about including raw sensitive values in keys or diagnostics. You usually need a bounded representation of context, not complete tokens or request bodies. Define what can safely appear in logs and shared cache administration tools. Key design is both a correctness decision and a data-handling decision.

Also consider key cardinality. Caching every arbitrary search string forever can become an unbounded memory problem. A useful cache needs a size or eviction policy as well as freshness. Determine whether a small set of frequent queries justifies reuse before treating every request as something worth storing.

Choose expiration, invalidation or no cache

Expiration is simple when a bounded stale interval is acceptable. Event-driven invalidation may be appropriate when a successful edit must become visible promptly. Some operations should remain uncached because their correctness or low cost makes reuse unnecessary. Choose among these options from the requirement, not from a desire to use every available framework feature.

An illustrative policy note can make the decision reviewable:

Resource: public project description
Key: project ID plus locale
Owner: publishing service
Freshness: bounded delay acceptable for public readers
After save: author view reads the persisted document directly
Invalidation: publishing service expires the public entry
Failure: serve a clear error if no trustworthy value exists

Adapt each line to the actual product. The note does not implement a cache; it identifies what the implementation must preserve. If the requirement remains ambiguous, resolve it with the product owner before burying the uncertainty inside a timeout value.

Consider concurrent refreshes too. Many requests can discover expiration together and repeat the same expensive work. A bounded single-flight mechanism or controlled refresh strategy may help, but it adds coordination behavior that needs testing. Do not introduce distributed locks before establishing that duplicate refresh cost is a real problem.

Define what happens when the source fails

Decide whether stale data is safer than an error for the specific use. A public informational page may reasonably show an older verified result with an appropriate policy. A permission check may need to fail closed. The same fallback cannot safely serve both cases simply because stale content improves availability.

If stale-on-error behavior is allowed, define its maximum age and visibility. Otherwise a temporary outage can extend an entry indefinitely. Make observability distinguish fresh hits, stale fallbacks, misses, and source failures. A high hit rate is not automatically good if many hits violate the intended freshness contract.

Keep fallback decisions separate from optimistic UI. Showing an optimistic edit is a presentation choice; it does not prove that a cached server result is correct. When persistence fails, the interface needs a way to recover without presenting the optimistic value as confirmed data.

Use AI to inspect invalidation paths

Provide the key construction, readers, writers, and documented freshness policy. Ask an assistant to enumerate events that can change the result and identify paths that fail to invalidate or bypass appropriately. Give it concrete examples of two users, two locales, and a recently edited resource so missing key dimensions become visible.

Require evidence from the code and reject proposals that merely shorten every timeout. An assistant can also help create counterexamples, such as deleting a record while its public entry remains cached. Keep any realistic data synthetic and redact identifiers that are unnecessary to the reasoning.

Test behavior at the boundaries

Verify an initial miss, repeated hit, successful edit, failed edit, expiration, deletion, authorization change, and source failure. Confirm the user-facing result matches the policy in each case. Include a test for the key dimension most likely to be forgotten, such as tenant or locale.

Measure whether caching meaningfully reduces the cost you intended to address. If it saves little while adding difficult invalidation, remove or narrow it. The simplest useful cache has a known owner, bounded resource use, and a freshness contract that the team can explain. Correctness remains part of performance engineering even when the response becomes much faster.

Sources

About the Author

Kamruz

Kamruz

Full Stack Developer

A passionate developer who loves creating modern web applications and sharing knowledge with the community.

More About Me

Related Articles