Programming · free software · the open webSubscribe via RSS

A cache key is a claim that two requests mean the same thing

A cache key decides which computations or responses may be reused. Treat it as an explicit equivalence rule, and separate identity, freshness, privacy and invalidation when deciding whether reuse is correct.

Reuse starts with an equivalence rule

A cache does not know whether two requests are interchangeable. It looks up a key and reuses the value associated with it. The key therefore makes a claim: every input omitted from the key is either irrelevant to the result or handled by another correctness rule. That is true for an HTTP response cache, a memoized function and a hand-built dictionary alike.

Suppose a service renders a product page from a path and a language preference. A key containing only the path treats requests for English and French as equivalent. That is correct only if the representation is the same for both. If the server selects language from Accept-Language, HTTP’s Vary field tells caches that this header can affect selection; the cache must compare that field before reusing a stored response. [2] [1]

List the inputs that can change the value

For a computed value, write down the function’s effective inputs, not just its formal parameters. A price quote might depend on product ID, currency, customer tier and a pricing-rule revision. A function that reads a global locale, current time, environment variable or mutable object has inputs that its signature hides. If one of those can change the answer while the key stays fixed, the cache can return a value for a different computation.

This is why small memoization examples are safer than broad service caches. Python’s functools.lru_cache keys calls from their arguments, and its documentation notes that distinct argument patterns can occupy separate entries; calls with the same keyword arguments in different orders may therefore not share an entry. That is an efficiency difference, not automatically a correctness bug. More dangerous is wrapping a function whose result depends on state that is not in its arguments, then assuming the decorator can discover that dependency. [3]

Identity and freshness solve different problems

Adding a dimension to the key separates two kinds of result. Expiring or invalidating an entry decides how long a result may be reused after its inputs have not changed. These mechanisms address different questions. A key that omits the account ID can mix two users immediately; a short time-to-live merely limits how long that mix-up lasts. Conversely, a complete key can still return old data after the underlying record changes unless the cache has a freshness or invalidation rule.

A practical design often uses a stable revision as an input: (document_id, document_version, renderer_version). Updating the document or renderer creates a different identity, so old entries become unreachable and can be removed later as storage cleanup. This works well when versions are cheap to obtain and change whenever relevant meaning changes. If version lookup is itself stale, or a dependency changes outside the versioning system, the apparent precision is illusory.

Make privacy boundaries explicit

A cache can be wrong even when it returns a technically valid representation: it may return one user’s private response to another. Shared caches have HTTP rules for responses to requests with Authorization; they generally cannot reuse such a stored response unless the response explicitly permits shared caching under the specified directives. These protocol rules matter, but an application cache still needs its own ownership model. [1]

For application data, partition by an authorization-relevant identity when sharing is intended and safe, or bypass caching for sensitive results. Do not use a raw bearer token as a key: it is secret material, rotates independently of the underlying identity and can leak through logs or diagnostics. A stable internal principal identifier may be appropriate, provided permission changes also invalidate or version the result. The key should encode the policy boundary the result depends on, not credentials themselves.

Review the key as part of the API contract

A useful review asks four questions. Which observable inputs select the result? Which of them are represented in the key? What rule handles changes after insertion: a version, explicit invalidation, validation or bounded freshness? Finally, can two security principals ever reach the same entry? These questions expose hidden dependencies before performance tuning makes them harder to see.

HTTP makes this contract visible through method, target URI and, when applicable, request fields named by Vary; Vary: * indicates that a cache cannot establish a match on request fields alone. An in-process cache expresses the same design in a tuple, object identity or function arguments. A hit is evidence only that keys matched. Correct reuse still depends on whether the key captures the distinctions the computation promises to respect. [1] [2]

References

  1. RFC 9111: HTTP Caching
  2. RFC 9110: HTTP Semantics, Vary
  3. Python functools documentation

Suggest a correction · All posts