How Heuristic Freshness Is Calculated
Problem Statement
A page is being served from cache and nobody configured a lifetime for it. You need to know how long that will continue — not approximately, but well enough to decide whether the current behaviour is tolerable while a proper header is deployed. The number is derivable from the response itself, and this page walks the calculation with real values.
Prerequisite Concepts
- Heuristic freshness and implicit caching — the rule that permits a cache to invent a lifetime, and the status codes it applies to.
- How to calculate cache freshness lifetime — the full RFC 9111 precedence chain, of which the heuristic is the last branch.
- How to read and interpret the Age header —
Ageis the only observable that lets you confirm the calculation.
Step-by-Step Resolution
Step 1 — Confirm there is no explicit lifetime
The heuristic is only reachable when every explicit source is absent. Capture the full header set first:
curl -sI https://example.com/guides/setup \
| grep -iE '^date|^last-modified|cache-control|expires|^age|^etag'
A representative directive-free response:
Date: Fri, 31 Jul 2026 09:00:00 GMT
Last-Modified: Thu, 02 Jul 2026 09:00:00 GMT
ETag: "3a91-2f0"
Age: 4210
No Cache-Control and no Expires. If either had been present with a lifetime, the heuristic would never be consulted and the rest of this procedure would not apply.
Step 2 — Measure the Last-Modified interval
The heuristic derives from how long the resource has gone without changing, measured as Date minus Last-Modified:
D=$(curl -sI https://example.com/guides/setup | awk -F': ' '/^[Dd]ate/{print $2}')
L=$(curl -sI https://example.com/guides/setup | awk -F': ' '/^[Ll]ast-[Mm]odified/{print $2}')
echo $(( $(date -d "$D" +%s) - $(date -d "$L" +%s) )) seconds
For the response above the interval is 29 days, or 2,505,600 seconds.
Step 3 — Apply the implementation’s fraction
Multiply by the fraction the cache in the path uses. The widely-inherited convention is one tenth:
freshness_lifetime = (Date - Last-Modified) / 10
= 2505600 / 10
= 250560 seconds ≈ 2.9 days
Then clamp to the implementation’s ceiling. Squid’s default maximum is 259,200 seconds — three days — so in this case the clamp barely applies. A resource unchanged for a year would produce 3.6 million seconds before clamping and would be capped at the configured maximum instead.
Step 4 — Compare against the current Age
Age tells you how much of that lifetime has already elapsed:
remaining = 250560 - 4210 = 246350 seconds ≈ 2.85 days
So the copy currently being served will continue to be served, without any origin contact, for nearly three more days.
Step 5 — Confirm empirically
The calculation predicts a number; observation confirms which implementation is actually in the path. Poll at intervals and record the Age at which it resets:
while true; do
printf '%s ' "$(date -u +%H:%M:%S)"
curl -sI https://example.com/guides/setup | awk -F': ' '/^[Aa]ge/{print $2}'
sleep 300
done
An Age that climbs steadily and then drops to a low value marks the moment the lifetime expired and the cache revalidated. If the reset happens near your calculated figure, the convention holds for this path. If it happens at a round number — 120, 3600 — you are behind a cache using a fixed default TTL rather than a derived one.
Expected Output / Verification
A correct diagnosis produces three consistent facts:
- No
Cache-Controllifetime and noExpiresheader in the captured response. - An
Agevalue that climbs monotonically across spaced requests, proving a shared cache is storing the response. - A reset point that matches either the calculated fraction of the
Last-Modifiedinterval, or the configured default TTL of the cache in the path.
HTTP/2 200
Date: Fri, 31 Jul 2026 12:00:00 GMT
Last-Modified: Thu, 02 Jul 2026 09:00:00 GMT
Age: 250480
An Age of 250,480 against a calculated lifetime of 250,560 means this entry is 80 seconds from expiry — the clearest possible confirmation that the convention is being applied.
Edge Cases
- No
Last-Modifiedheader. There is no interval to derive from. Squid falls back to a configured minimum, Varnish and Fastly apply theirdefault_ttlregardless, and some caches decline to store the response at all. Observation is the only way to tell which you have. - A
Last-Modifiedin the future. Clock skew at the origin can produce a negative interval. Implementations clamp to zero, which makes the response immediately stale — the opposite of the usual symptom, and easily mistaken for a caching failure. - Two tiers, two lifetimes. A browser and an edge node can apply different heuristics to the same response, so the effective staleness a user sees is whichever tier answered. This is why a change can appear for one visitor and not another with no pattern in between.
- A
304resets the calculation. Revalidation refreshesDate, so the interval shrinks and the next heuristic lifetime is shorter. A frequently revalidated resource therefore drifts toward shorter heuristic lifetimes over time.
Frequently Asked Questions
Which fraction of the Last-Modified interval is used?
One tenth is the inherited convention and the default in Squid and most standards-following caches, bounded by configurable minimum and maximum values. It is not specified by RFC 9111, so confirm it empirically for the cache actually in your path.
What happens if Last-Modified is missing?
There is nothing to derive from. Some implementations apply a fixed default TTL, others decline to store the response. Varnish and Fastly use their configured default_ttl regardless of Last-Modified, which is predictable but unrelated to how often the resource changes.
Does the heuristic recalculate on each request?
No. The lifetime is fixed when the entry is stored. A successful revalidation refreshes the entry and recomputes the heuristic from the new Date, which usually yields a shorter figure than the original.
Can the calculated lifetime exceed a day?
Routinely. A month of stability yields three days under the one-tenth convention, and a year yields over a month before clamping. RFC 9111 asks for a warning above 24 hours, but almost nothing surfaces it.
Related
- Cache-Control vs Expires: which wins? covers the two explicit sources whose presence removes the heuristic from consideration.
- Handling clock skew in Last-Modified validation explains the drift that distorts the interval this calculation depends on.
- Interpreting X-Cache and CF-Cache-Status headers identifies which tier is holding the entry you are measuring.