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

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.

The three quantities the calculation needsThe interval between Last-Modified and Date is scaled by the implementation's fraction to produce the lifetime, which Age is then compared against.Interval2505600 sDate - Last-ModifiedFractionone tenthmultiplyLifetime250560 scompareAge now4210 s
Interval, fraction, lifetime — then compare against Age

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.

Heuristic lifetime by how long the resource has been stableBecause the lifetime is a fraction of the stability interval, older content is cached far longer than recently edited content, with no directive involved.Changed 1 hour ago0.1hoursChanged 1 day ago2.4hoursChanged 1 week ago16.8hoursChanged 1 month ago72hoursChanged 1 year ago876hours before clampingthe same page, cached for wildly different durations
Lifetime assigned under the one-tenth convention

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:

What each implementation actually does with a directive-free responseFour caches given the same headerless response derive four different lifetimes, which is why the calculated figure has to be confirmed against observed behaviour.BasisLifetime for a 29-day-old resourceSquid10% of the Last-Modified interval~2.9 days, clamped to 3Varnishfixed default_ttl120 secondsFastlyconfigured service defaultoften 1 hourCloudflarefile extension policycached only for static types
The same response, four different assigned lifetimes
  • No Cache-Control lifetime and no Expires header in the captured response.
  • An Age value 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-Modified interval, 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-Modified header. There is no interval to derive from. Squid falls back to a configured minimum, Varnish and Fastly apply their default_ttl regardless, and some caches decline to store the response at all. Observation is the only way to tell which you have.
  • A Last-Modified in 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 304 resets the calculation. Revalidation refreshes Date, 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.


Back to Heuristic Freshness and Implicit Caching