The problem with manual invalidation
Every time you deploy, some of your static assets stay cached on CloudFront's edge nodes for hours or days. Visitors keep seeing old CSS and JavaScript until either:
- CloudFront's TTL expires (could be 24 hours or more)
- You manually create an invalidation (costs money per file).
You have to remember to do this after every deploy. If you forget, old JavaScript is loaded alongside new HTML, breaking the site until you fix it manually.
The real problem: CloudFront doesn't know which files have actually changed and which haven't. So it has to choose: cache everything long (save money, break deployments) or cache everything short (constant origin requests).
Vite's content hashing: fingerprinting your assets
Vite bakes a hash of each file's contents into the filename:
// Before build src/main.js // After Vite build dist/assets/main-8f4a3c.js
The hash changes only when the file contents change. If you build twice without changing code, you get the same hash. If you change one line, the hash changes completely.
This is the key insight: the filename IS the version number. A file with a hash is immutable — main-8f4a3c.js will always contain exactly the same code.
Two-tier cache strategy
Tier 1: HTML files — revalidate on every request
Cache-Control: no-cache, must-revalidate
HTML files reference assets by their hashed filenames. Every time someone visits your site, the browser fetches the latest HTML (which now points to the latest asset filenames). CloudFront checks the origin to see if the HTML has changed. If it's fresh, the browser loads the new script/style references.
Tier 2: Hashed assets — cache forever
Cache-Control: max-age=31536000, immutable
main-8f4a3c.js will never change. If the code changes, Vite produces a new filename like main-a2b9e.js. The old cached file stays cached forever — no viewers, no wasted bandwidth. The new HTML file will reference the new filename, and CloudFront caches that new asset for a year.
Browsers see immutable and know they don't need to revalidate. CloudFront respects the 1-year TTL. No invalidation ever needed.
nginx: enforce cache-control headers
Your nginx needs to send these headers correctly:
location ~* \.(js|css|woff2|svg)$ {
# Hashed assets: cache forever
add_header Cache-Control "max-age=31536000, immutable" always;
}
location ~* \.(html)$ {
# HTML files: always revalidate
add_header Cache-Control "no-cache, must-revalidate" always;
}
location / {
# Everything else: sensible default
add_header Cache-Control "public, max-age=3600" always;
}
nginx now sends the right headers. CloudFront picks them up (using the UseOriginCacheControlHeaders policy), and enforces the caching strategy across all edge nodes.
A deploy means new HTML (always served fresh) that references new asset filenames (cached forever on the CDN). No invalidation, no downtime, no manual steps.