Updated:

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:

  1. HTML — should update quickly after deployment.
  2. 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 typeExampleCache-Control
HTML/, /en/docs/public, must-revalidate
Assets/assets/app.ABC123.jspublic, max-age=31536000, immutable
404/no-such-pagepublic, must-revalidate
Sitemap/sitemap-index.xmlpublic, must-revalidate
robots.txt/robots.txtpublic, 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