Cache-Control
Cache is not just “for speed”. It is also about predictable behavior after deployment.
A static site usually has two different file types:
- HTML — should update quickly after deployment.
- Hashed assets — CSS/JS files whose names change when their contents change. These can be cached for a long time.
If these rules are mixed up, you get strange bugs: old pages with new styles, or new HTML pointing to old JavaScript.
Good baseline
| File type | Example | Cache-Control |
|---|---|---|
| HTML | /, /en/docs/ | public, must-revalidate |
| Assets | /assets/app.ABC123.js | public, max-age=31536000, immutable |
| 404 | /no-such-page | public, must-revalidate |
| Sitemap | /sitemap-index.xml | public, must-revalidate |
| robots.txt | /robots.txt | public, must-revalidate |
Why HTML should not be cached for too long
HTML is the entry point. It references the current CSS and JavaScript files.
If HTML uses this:
Cache-Control: public, max-age=31536000, immutable
the browser may keep showing an old page after deployment.
For HTML, use:
Cache-Control: public, must-revalidate
This allows the browser to store the file but requires it to revalidate freshness.
Why assets can use long cache
Astro builds CSS/JS with hashes in filenames. Example:
/assets/cache-control.B1dARQ-M.css
/assets/hoisted.C4vOHhWK.js
When the content changes, the filename changes too. That makes long asset caching safe.
For assets, use:
Cache-Control: public, max-age=31536000, immutable
immutable means: “as long as the URL is the same, the file will not change”.
Nginx example
For assets:
location /assets/ {
try_files $uri =404;
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header X-Content-Type-Options nosniff always;
}
For HTML and normal paths:
location / {
try_files $uri $uri/ =404;
add_header Cache-Control "public, must-revalidate" always;
add_header X-Content-Type-Options nosniff always;
}
For 404:
error_page 404 /404.html;
location = /404.html {
internal;
add_header Cache-Control "public, must-revalidate" always;
}
Check HTML
curl -kI https://getsrv.app/
Expected:
HTTP/2 200
content-type: text/html
cache-control: public, must-revalidate
Check assets
ASSET="$(find /var/www/getsrv.app/assets -maxdepth 1 -type f | head -n 1 | sed 's#^/var/www/getsrv.app##')"
curl -kI "https://getsrv.app$ASSET"
Expected:
HTTP/2 200
cache-control: public, max-age=31536000, immutable
Check 404
curl -kI https://getsrv.app/no-such-page
Expected:
HTTP/2 404
cache-control: public, must-revalidate
Check sitemap
curl -kI https://getsrv.app/sitemap-index.xml
Expected:
HTTP/2 200
content-type: text/xml
cache-control: public, must-revalidate
Common mistakes
Mistake 1. Using expires and add_header Cache-Control together
In Nginx, expires also generates Cache-Control. If you add another Cache-Control manually, you can end up with duplicate headers.
Bad:
expires 1h;
add_header Cache-Control "public, must-revalidate" always;
Choose one method. For precise control, using only add_header Cache-Control is clearer.
Mistake 2. Using immutable for HTML
Bad:
location / {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
The browser may keep old HTML for too long.
Mistake 3. Assets are not under /assets/
If the build tool writes assets to one path but Nginx caches another path, the cache rules will not apply.
Check Astro:
build: {
assets: 'assets'
}
And Nginx:
location /assets/ {
...
}
Mistake 4. Old HTML references deleted JS
If rsync --delete is used, old assets are removed. That is correct, but if HTML is cached for too long, the browser may open old HTML that references a deleted JS file.
That is why HTML should use must-revalidate, while assets can use immutable.
Short post-deploy check
curl -kI https://getsrv.app/ | grep -i cache-control
ASSET="$(find /var/www/getsrv.app/assets -maxdepth 1 -type f | head -n 1 | sed 's#^/var/www/getsrv.app##')"
curl -kI "https://getsrv.app$ASSET" | grep -i cache-control
curl -kI https://getsrv.app/no-such-page | grep -i cache-control
Expected meaning:
HTML: public, must-revalidate
Asset: public, max-age=31536000, immutable
404: public, must-revalidate