Long Cache TTL (Time To Live) means setting a long Cache-Control: max-age on static assets so that repeat visitors don't re-download them on every visit. The recommended value for hashed static assets is one year — 31,536,000 seconds. Audits flag any static resource served with a TTL under 30 days because the user is paying a network cost they shouldn't have to.
immutable directive: Cache-Control: public, max-age=31536000, immutable tells the browser it never needs to revalidate — saving even the conditional request.no-cache or 60–300 s) so users always fetch the version that points to the current asset hashes.main.a3f7c2.js) make cache invalidation automatic when content changes.Think of caching like leaving your shoes by the front door. If they're right there, you can grab them instantly on the way out. If you have to fetch them from the closet every single time, every trip starts slower. A long cache TTL is the same idea: keep the assets the user already downloaded close by, and don't make them go get a fresh copy unless something has actually changed.
A short or missing cache TTL means every page view re-downloads CSS, JS, fonts, and images that haven't changed. The cost compounds across five places:
The reason a 1-year cache TTL is safe is because production builds emit asset filenames containing a content hash. When the file's contents change, the hash changes — and the URL becomes a brand-new resource the browser has never cached.
<!-- Today's build -->
<script src="/assets/main.a3f7c2.js"></script>
<!-- Tomorrow's build (after a code change) -->
<script src="/assets/main.b91d44.js"></script>Because the URL itself changed, there is no "stale cache" problem — the browser simply has no copy of main.b91d44.js and fetches it fresh. The previous file (main.a3f7c2.js) sits unused in the cache until eviction; that costs nothing because the user never requests it again.
This is why the right Cache-Control header for hashed assets is the most aggressive one available:
Cache-Control: public, max-age=31536000, immutablepublic — any cache (browser or shared) may store it.max-age=31536000 — store it for one year.immutable — promise the browser this URL will never serve different bytes, so it never needs to send a conditional revalidation request.HTML should typically not have a long cache TTL. The HTML document is the entry point — it's where the hashed asset URLs are referenced. If you long-cache the HTML, users can be stuck pointing at old asset hashes that no longer exist on disk. Set HTML to Cache-Control: no-cache or a short TTL (60–300 seconds) so the browser revalidates the entry point on each visit while still long-caching the assets it references.
Cache TTL is set per response by the server (or origin behind a CDN). The fastest ways to audit it:
Cache-Control. Any static asset under 30 days is a regression.Apply the strongest Cache-Control to anything with a content hash in the filename (JS bundles, CSS bundles, fonts, hashed images).
Cache-Control: public, max-age=31536000, immutableHTML is the entry point — it must always reflect the current asset hashes. Set a short TTL or use no-cache to force revalidation on each visit.
Cache-Control: no-cache
# or
Cache-Control: public, max-age=300, must-revalidateIn Nginx, set headers per location block so hashed assets get the long TTL and HTML doesn't.
location ~* \.(js|css|woff2|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location / {
expires -1;
add_header Cache-Control "no-cache";
}The same idea using mod_expires and mod_headers:
<FilesMatch "\.(js|css|woff2|png|jpg|svg)$">
ExpiresActive On
ExpiresDefault "access plus 1 year"
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>webpack, esbuild, vite, and most modern bundlers emit hashed filenames by default. If yours doesn't, enable it — long caching without hashing is unsafe.
immutable tells the browser the URL never serves different bytes, so it skips the conditional revalidation request entirely. Only use it on truly hashed (versioned) URLs.
If a file is named main.jswith no hash, you cannot safely long-cache it — once it's in user caches you have no way to invalidate it. Either add a hash to the build output or use a short TTL.
If you're behind a CDN, the edge can override origin headers. Configure cache rules at the edge so hashed paths automatically receive the year-long TTL even if origin defaults are shorter.
What's happening: Out of fear of cache invalidation issues, every static asset is given a 1-day or 1-hour TTL. Repeat visitors re-download everything constantly.
Fix: Adopt content-hashed filenames in your build pipeline, then long-cache hashed assets at 1 year with immutable. Short caching is the wrong tool for the job.
What's happening: The build emits app.js with no hash, so the team is forced to use a short TTL — because there's no way to invalidate cached copies after a deploy.
Fix: Configure your bundler to output content-hashed filenames (e.g., app.[contenthash].js). Then safely raise the TTL.
What's happening: A misconfigured build uses a deterministic but content-insensitive name. Files change, hash doesn't — caches serve stale code forever.
Fix: Switch from [name] to [contenthash] in the bundler output template. Verify hashes change after a deliberate file edit before relying on long caching.
What's happening: A blanket cache rule applies max-age=31536000 to every response — including HTML. Users keep loading old HTML that points at deleted asset hashes, breaking the site.
Fix: Scope the long-TTL rule to file extensions or paths that contain hashed assets. Use no-cache or a short TTL for HTML.
Different resource types need different cache policies. Use this table as the default starting point.
| Resource Type | Recommended TTL | Cache-Control Header | Why |
|---|---|---|---|
| Hashed JS / CSS bundles | 1 year | public, max-age=31536000, immutable | URL changes whenever content changes — safe to cache forever. |
| Hashed images / fonts | 1 year | public, max-age=31536000, immutable | Same as bundles — URL is the cache-busting mechanism. |
| Unhashed static assets | 1 hour – 1 day | public, max-age=3600 | No way to force-invalidate; keep TTL short. |
| HTML documents | 0 – 5 minutes | no-cache or max-age=300, must-revalidate | Entry point — must reference current asset hashes. |
| API responses (JSON) | Depends on data freshness | private, max-age=60 or no-store | Often user-specific or rapidly changing. |
| Service Worker file | 0 | no-cache | Must update immediately to ship new offline logic. |
One year — 31,536,000 seconds — combined with content-hashed filenames. The full recommended header is Cache-Control: public, max-age=31536000, immutable. Audits flag any static resource with a TTL under 30 days.
immutable tells the browser the URL will never serve different bytes. With it set, the browser doesn't even send a conditional revalidation request — it just uses the cached copy until max-age expires. Use it only on URLs that change when content changes (i.e., hashed asset URLs).
No. HTML is the entry point that references hashed asset URLs. If HTML is long-cached, users can get stuck pointing at deleted hashes. Use Cache-Control: no-cache or a short max-age (60–300 seconds) so the browser revalidates HTML on each visit.
Change the URL. Because production assets are content-hashed, any code change automatically produces a new filename. Users will request the new URL on their next visit; old URLs sit unused in caches until eviction. There is no "purge" needed.
max-age is the modern way to set TTL in seconds. Expires is the legacy header expressing an absolute date. s-maxage applies only to shared caches (CDNs, proxies) and overrides max-age there. Prefer max-age for browser caching and add s-maxage only when you need different shared-cache behaviour.
Indirectly. AI answer engines and Google AI Overviews preferentially cite pages that already rank well — and short-TTL repeat-visit penalties feed into Core Web Vitals and Page Experience scores. Faster repeat visits also mean Googlebot and AI crawlers pull less data per crawl, reducing crawl budget pressure on your origin.
Only if you long-cache without content-hashing. The combination of max-age=31536000, immutable + hashed filenames is safe by construction: every deploy emits new URLs the browser has never cached. The risk only appears if you long-cache unhashed filenames.
Long Cache TTL is one of the highest-ROI performance fixes available — it turns a 30–60% slice of your traffic (repeat visitors) into near-instant page loads, with no architectural changes. The pattern is simple: long-cache hashed assets with max-age=31536000, immutable, short-cache HTML so users always pick up the latest asset hashes, and let your bundler handle invalidation through content hashes automatically.
Run a Greadme deep scan to see exactly which assets on your site are missing the recommended TTL and ship a one-click fix.