Set HTTP Cache Headers for Static Assets and HTML
Give fingerprinted assets a long immutable cache while forcing browsers and CDNs to revalidate HTML, preventing stale deployments without sacrificing repeat-visit performance.
Task
Configure response headers so hashed assets are cached for a year and HTML entry points always revalidate.
When to use this
A deployment replaces JavaScript bundles, but browsers keep serving old HTML that references deleted assets, or every asset is downloaded again on each visit.
Prerequisites
- A web server or framework that can set response headers
- Build output with content-hashed asset filenames
- Access to the CDN configuration if one is used
Steps
1. Confirm that asset filenames contain a content hash. Only then is a long immutable cache safe.
ls dist/assets | headExpected output: Filenames such as app.3f9c2a1b.js with a content hash.
2. Serve hashed assets with a one-year immutable cache header.
app.use('/assets', express.static('dist/assets', {
immutable: true,
maxAge: '1y',
}));
Expected output: Response includes Cache-Control: public, max-age=31536000, immutable.
3. Serve HTML with revalidation so a new deployment is visible immediately. Avoid long max-age on HTML.
app.get('*', (_req, res) => {
res.set('Cache-Control', 'no-cache');
res.sendFile(path.resolve('dist/index.html'));
});
Expected output: HTML responses include Cache-Control: no-cache and revalidate with ETag/Last-Modified.
4. Verify headers with curl and confirm a deploy changes the HTML while hashed assets remain cached.
curl -I https://example.com/assets/app.3f9c2a1b.js && curl -I https://example.com/Expected output: Asset has max-age=31536000, immutable; HTML has no-cache.
Troubleshooting
Problem
Old HTML references deleted assets after deploy
Solution
HTML is being cached too long. Switch entry HTML to no-cache or a very short max-age and ensure the CDN honors origin headers.
Problem
Assets change without a hash change
Solution
Disable long caching until the build emits content-hashed filenames.
Limitations
- CDN behavior may override origin headers; verify the edge response, not only the local server.
- Immutable caching cannot be revoked for a URL; use a new hash instead.
The two-class policy
Treat HTML and hashed assets differently. HTML is the entry point and must update on deploy; hashed assets are content-addressed and can be cached forever because a change produces a new URL.
app.use('/assets', express.static('dist/assets', {
immutable: true,
maxAge: '1y',
}));
app.get('*', (_req, res) => {
res.set('Cache-Control', 'no-cache');
res.sendFile(path.resolve('dist/index.html'));
});
Verify
curl -Ion an asset showsCache-Control: public, max-age=31536000, immutable.curl -Ion/showsCache-Control: no-cacheand an ETag or Last-Modified.- After a deploy, a normal reload fetches the new HTML and references new hashed assets.
- The CDN edge response matches the origin policy.
Pitfalls
no-cache does not mean “do not store”; it means “revalidate before use.” no-store prevents caching entirely and is rarely needed for HTML. If your CDN strips or overrides origin headers, configure the cache policy at the edge as well. Never give HTML a one-year immutable cache unless you can change the URL on every deploy.