What the Cache Headers Actually Mean, and Which One Wins
Four headers set the caching behaviour and they disagree, so the effective policy is a precedence order rather than a sum, and the order is not what the…
Start at the cached and you miss it. Start at the incident and you miss how it arrived. The bug lived in the hand-off.
Here is the shape of the cache header precedence, and the one property that makes it confusing: the headers are not additive, so setting several does not combine them, it selects one behaviour by a precedence rule that the names do not reveal.
This applies to any HTTP response, and the mechanism is the specification's precedence, which is the same regardless of the server.
The two kinds of caching
The freshness is the first kind, and the freshness is the how long the copy is usable, and the usable is the no revalidation, and the no revalidation is the speed, and the speed is the freshness's value, and the value is the Cache-Control's max-age, and the max-age is the seconds, and the seconds is the window, and the window is the stale's start.
The validation is the second kind, and the validation is the check with the origin, and the check is the conditional request, and the conditional is the ETag or the Last-Modified, and the two are the validators, and the validators are the 304, and the 304 is the no body, and the no body is the saving, and the saving is the validation's value, and the value is the smaller than the freshness's, because the request still happens.
The two kinds are the different mechanisms, and the different is the confusion, because the headers look like one system, and the one system is the assumption, and the assumption is the wrong, and the wrong is the incident, and the incident is the reason to know which mechanism each header belongs to.
The precedence that decides
Cache-Control beats Expires. The Expires is the absolute date, and the date is the old mechanism, and the old is the ignored when the max-age is present, and the present is the Cache-Control, and the Cache-Control is the winner, and the winner is the relative, and the relative is the better, because the absolute date breaks when the client's clock is wrong, and the wrong clock is the common.
no-store beats everything. The no-store is the no cache, and the no cache is the no copy, and the no copy is the every request to the origin, and the origin is the slow, and the slow is the price, and the price is the security's, and the security's is the reason the no-store exists, and the exists is the override, and the override is the total.
private beats shared. The private is the browser only, and the only is the CDN's exclusion, and the exclusion is the shared cache's skip, and the skip is the per user, and the per user is the reason, and the reason is the personalisation, and the personalisation is the privacy, and the privacy is the private's purpose.
must-revalidate beats the stale serving. The must-revalidate is the no stale, and the no stale is the origin's requirement, and the requirement is the 503 rather than the old, and the old is the forbidden, and the forbidden is the correctness, and the correctness is the reason, and the reason is the financial data, and the data is the must's case.
The validator is used after the freshness expires. The max-age is the window, and the window's end is the revalidation, and the revalidation is the ETag, and the ETag is the conditional, and the conditional is the 304 or the fresh copy, and the two are the outcome, and the outcome is the validator's role, and the role is the after rather than the instead.
Why the combination surprises
The four headers together are the precedence, and the precedence is the one behaviour, and the one behaviour is the not the sum, and the not sum is the surprise, and the surprise is the stale, and the stale is the incident, and the incident is the reason to set the one header deliberately rather than the several hopefully.
The common mistake is the Expires with the max-age, and the with is the conflict, and the conflict is the max-age's win, and the win is the invisible, and the invisible is the assumption that the Expires matters, and the matters is the wrong, and the wrong is the confusion, and the confusion is the reason to drop the Expires.
This is the same layering as the rollback where the CDN kept serving the old version, which is why the two fixes look alike.
What actually fixed it
Set one policy deliberately. The Cache-Control is the single source, and the single is the clear, and the clear is the predictable, and the predictable is the fix, and the fix is the removal of the conflicting headers, and the removal is the clarity.
Use max-age for the freshness and the ETag for the validation. The two are the mechanisms, and the mechanisms are the complementary, and the complementary is the fresh window then the cheap check, and the check is the 304, and the 304 is the saving, and the saving is the design.
Mark the personal responses private. The private is the CDN's exclusion, and the exclusion is the user's isolation, and the isolation is the fix, and the fix is the one directive, and the directive is the privacy, and the privacy is the discipline, because the shared cache serving the personal data is the leak.
Purge deliberately rather than relying on expiry. The purge is the immediate, and the immediate is the update's visibility, and the visibility is the fix, and the fix is the CDN's API, and the API is the deployment's step, and the step is the discipline, because the expiry is the wait and the wait is the stale.
Verify the effective policy from the client. The response headers are the check, and the check is the reality, and the reality is the cache's behaviour, and the behaviour is the fix's verification, and the verification is the curl with the headers printed, and the printed is the evidence.
The rule I keep
Cache headers are not additive: Cache-Control beats Expires, no-store overrides everything, private excludes shared caches, must-revalidate forbids stale serving, and the validators are used only after the freshness window ends. Set one deliberate policy, use max-age with an ETag, mark personal responses private, and purge rather than waiting for expiry.
The response carried four cache headers and they did not combine into one policy, because the precedence picked a winner and the winner was not the one the author meant. The lesson I keep is the property, not the incident.