Performance

Cache-Control explained: headers, max-age and immutable

Cache-Control decides how long a browser and a CDN reuse a response. HTML with no-cache, hashed assets as immutable: the setup that holds.

Key points

  • Serve HTML with no-cache and hashed assets with a long max-age plus immutable.
  • On 1 November 2026 the hashed asset answered immutable with cf-cache-status HIT, the HTML no-cache with DYNAMIC.
  • immutable is safe only when the file name changes on every build.

Cache-Control tells the browser and the CDN how long they may reuse a response without asking the server. One setup holds: HTML with no-cache, assets with a hash in the name and max-age=31536000, immutable.

The directives that matter

DirectiveWhat it doesWhere to use it
no-cachestores the response and revalidates it on every useHTML and dynamic APIs
no-storestores nothingresponses with personal data
max-age=31536000365 days of freshnessassets with a hash in the name
immutableno revalidation, not even on reloadassets with a hash in the name
stale-while-revalidateserves the old copy while it refreshesimages and non-critical JSON

The command

curl -sS -o /dev/null -D - -H "Accept-Encoding: br, gzip" https://ui.eneawork.it/assets/index-B90Ihhpp.js

The output shows the edge response, not the server one: -D - prints the headers and -o /dev/null discards the body.

Why the file name decides everything

A file with a hash in the name changes URL on every build, so it can be immutable. A file with a stable name, such as /css/app.css, must expire or be purged from the CDN on every deploy.

if (/\.(woff2?|ttf)$/i.test(filePath)) {
  res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
} else if (/\.(css|js|mjs)$/i.test(filePath)) {
  res.setHeader("Cache-Control", "public, max-age=2592000, stale-while-revalidate=604800");
} else {
  res.setHeader("Cache-Control", "no-cache");
}

This is the code that serves this site: fonts immutable for a year, CSS and JS without a hash for 30 days with background revalidation, everything else under no-cache. The deploy must purge the CDN for the files without a hash.

no-cache does not mean “do not store”

With no-cache the browser keeps the copy and asks the server to confirm it. With no-store it keeps nothing. With max-age=0 the copy is stale at once and gets revalidated: the difference from no-cache is more formal than practical.

The limitation

The measurement describes the edge, not the origin. Cloudflare answered public, max-age=0 to a HEAD without Accept-Encoding and no-cache to the GET with compression: same URL, two headers. A HIT on one point of presence says nothing about the others.

Where to continue

The EneUI catalogue indexes 111,985 components and 1,556 blocks with previews. The record spotlight card is a card with an image: change it and the URL stays, so the cache needs purging. For compression read Brotli versus gzip, for the Next.js cache layers the four layers, for bundle weight one import that grows the bundle.

Frequently asked questions

What is the difference between no-cache and no-store?

no-cache allows storing the response but forces revalidation before reuse. no-store forbids storing it, which makes it the choice for personal data.

Can I use immutable everywhere?

No. immutable is safe only when the URL changes with the content, so the file name needs a hash. On a stable name it blocks updates for a year.

Do I still need an ETag with immutable?

On an immutable asset the ETag is harmless but useless, because no client will use it. On HTML the ETag cuts the bytes of a revalidation, so keep it.

max-age or s-maxage?

s-maxage applies to shared caches such as a CDN and takes precedence over max-age. Use s-maxage when the CDN should keep a copy longer than the browser.

Why does the browser revalidate a long max-age?

It happens on a forced reload, or when the local cache is full and the browser evicts entries. With immutable a reload makes no request, but only if the earlier response is still cached.