Heuristic Freshness: What Happens When Cache-Control Is Missing
This guide explains a hidden behaviour behind many caching bugs, within HTTP Cache-Control Headers Explained and Advanced Caching Strategies & CDN Architecture. When a response has no Cache-Control max-age and no Expires, HTTP caches are allowed to pick a freshness lifetime themselves — "heuristic freshness". The common heuristic, suggested by the HTTP specification and used by browsers, is 10% of the time since Last-Modified. A file last modified 30 days ago gets roughly three days of freshness, without anyone having decided that.
That sounds harmless until a script or stylesheet with a stable filename is updated: some visitors keep the old version for days, mixing old JavaScript with new HTML. Or an API response with a Last-Modified header from long ago is reused for hours. The reverse also happens — responses that should be cached are not, because no Last-Modified is present. Heuristic caching is unpredictable by design, and the cure is to set explicit policies everywhere.
Rapid Diagnosis
- Scan responses for missing freshness: for each resource type, check whether
Cache-Control(withmax-age,s-maxage,no-cacheorno-store) orExpiresis present. - Look for stable-name assets (
/js/app.js,/css/main.css,/config.json) served without explicit headers. - Check DevTools. Resources served "from disk cache" with no
Cache-Controlheader are being cached heuristically. - Check CDN defaults. CDNs apply their own default TTLs to responses without headers, which may differ from browsers' heuristics.
Root Cause Analysis
1. Origins that send no caching headers. Default web server configurations often send Last-Modified but no Cache-Control.
2. Stable filenames. Without content hashes in names, heuristic caching keeps old versions after updates.
3. Different caches, different heuristics. Browsers, CDNs and proxies each choose their own lifetimes, so behaviour differs by path through the network.
4. Old Last-Modified dates. Files deployed with preserved old dates get long heuristic lifetimes.
Step-by-Step Resolution
1. Audit freshness headers by resource type
for u in / /css/main.css /js/app.js /api/config /images/logo.png; do
printf '%-20s ' "$u"
curl -sI "https://www.example.com$u" | grep -iE '^(cache-control|expires|last-modified):' | tr '\r\n' ' '
echo
done
# trade-off: a handful of URLs is a sample. Use CDN logs to list response
# headers by content type across all traffic for a complete picture.
2. Set explicit policies for every response class
location ~* \.[0-9a-f]{8,}\.(js|css|woff2|avif|webp|png|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable"; # hashed assets
}
location ~* \.(js|css)$ {
add_header Cache-Control "public, max-age=0, must-revalidate"; # unhashed: always revalidate
}
location /api/ { add_header Cache-Control "no-cache"; }
location / { add_header Cache-Control "public, max-age=0, s-maxage=300, stale-while-revalidate=3600"; }
# trade-off: regex location ordering in nginx matters; test that hashed assets
# match the first rule and not the unhashed one.
Expected outcome: every response states its intended lifetime; no cache has to guess.
3. Move stable-name assets to hashed filenames
Unhashed assets are the main victims of heuristic caching. Fingerprint them in the build so they can be cached for a year and updated instantly by changing the reference.
4. Set CDN defaults explicitly
Configure the CDN's default TTL for responses without headers (or to respect origin headers strictly) so behaviour is predictable.
Verification
Re-run the audit: every response class should carry explicit Cache-Control. Deploy a change to a previously unhashed file and confirm all clients pick it up immediately (because it is now hashed or revalidated). Check DevTools for resources cached without explicit headers — there should be none.
Worked Example: The Week-Long Stylesheet
A company site served /assets/site.css with Last-Modified but no Cache-Control. The stylesheet had not changed for eight months, giving it a heuristic lifetime of over three weeks in browsers. A redesign changed the HTML structure and the stylesheet together; returning visitors received new HTML with the old stylesheet and saw a broken layout for days. Support tickets spiked. The team fingerprinted all assets, served them as immutable, and set explicit revalidation for anything unhashed. The next redesign shipped without a single stale-asset complaint.
Heuristics and Performance
Heuristic freshness is not only a correctness hazard; it also leaves performance on the table. Responses without Last-Modified get no heuristic lifetime at all, so they are re-downloaded or revalidated on every use even if they never change. Explicit, long lifetimes for hashed assets and explicit revalidation for everything else is both faster and safer than letting caches guess. The audit in step 1 frequently uncovers fonts, images and scripts that could have been cached for a year and are instead being revalidated on every page view.
Common Mistakes
- Relying on
Expiresalone. It works but depends on clock accuracy; prefermax-age. - Assuming no header means no caching. It often means heuristic caching.
- Fixing HTML but not assets. Unhashed JS and CSS are where heuristic caching causes the worst breakage.
- Forgetting error responses. 404s and 5xx without headers may also be cached by some CDNs.
Edge Cases
Responses with status 200 vs 301/404. Heuristic caching applies to responses that are cacheable by default (including some redirects and 404s); give them explicit, short TTLs.
Query strings. Historically some caches refused heuristic caching for URLs with query strings; behaviour varies, another reason to be explicit.
Third-party assets. You cannot set headers on others' files; self-hosting or proxying lets you control them.
Range requests for media. Large media files without headers may be partially cached inconsistently; set explicit policies for media too.
FAQ
Is heuristic caching part of the HTTP standard?
Yes. The specification allows caches to assign heuristic freshness to responses without explicit lifetimes and suggests 10% of the time since Last-Modified as a typical value. It is legal and widely implemented, which is exactly why it causes surprises.
Does no-cache stop heuristic caching?
Yes — any explicit directive (no-cache, max-age, no-store) replaces the heuristic. Even max-age=0 is explicit.
Do CDNs use the same 10% heuristic?
Not necessarily. Many apply a configurable default TTL to responses without headers, or do not cache them. Check your CDN's documentation and configure it explicitly.
How can I see the heuristic lifetime a browser chose?
Browsers do not expose it directly. You can infer it from DevTools (whether a resource is served from cache without a request) and from the formula, but the practical answer is to remove the need by setting explicit headers.
Is it safe to add immutable to existing unhashed URLs?
No. immutable tells browsers never to revalidate during the TTL, so updates would not reach users. Use it only for content-addressed (hashed) URLs.
Which response types most often lack explicit headers?
In audits, the usual suspects are unhashed static files served by a default web server configuration, fonts, favicons and manifest files, JSON configuration fetched at startup, and redirect responses. Error pages generated by frameworks or load balancers are another common gap.
Can a CDN add headers for me?
Yes — most CDNs can set or override Cache-Control per path with rules or edge code. That is a quick fix for origins you cannot change, but keep the policy documented next to the origin configuration so the two do not drift apart.
Related
- Setting up immutable cache headers for hashed assets — the right policy for fingerprinted files.
- Invalidating immutable hashed assets safely — why hashing replaces invalidation.
- Cache-Control for HTML documents — the explicit HTML policy.