Learning objectives
After reading this article you will be able to:
- Understand what max-age, no-cache, no-store and immutable really do
- Pick the right Cache-Control for HTML, assets, images and API responses
- Set the header on a Node.js server and check the result with curl
Every time you open a website, the browser decides whether to download the files again or reuse a stored copy. The server makes that call through the Cache-Control header. Set well, it makes the site faster and cuts load on your server. Set badly, it leaves visitors looking at an old version for days.
If the CDN is still a black box to you, read what a CDN is first.
What does Cache-Control do?#
Cache-Control is an HTTP response header (and a request header too). It carries a comma-separated list of directives that apply to every cache on the path: the browser's (private, single user) and shared ones such as CDNs and reverse proxies. The rules are in RFC 9111.
HTTP/2 200
content-type: text/css
cache-control: public, max-age=31536000, immutableWith no freshness information at all, a cache may estimate a lifetime on its own (heuristic caching), usually a fraction of the time since the last modification. So the rule is: state what you want instead of leaving the choice to the cache.
Which directives matter day to day?#
max-age=N: the response is fresh for N seconds, counted from when the server generated it. Fresh means the cache serves it without asking the server anything.s-maxage=N: the same, but only for shared caches (CDNs). It overridesmax-agethere, and browsers ignore it.no-cache: may be stored, but must be revalidated with the server before every reuse. It does not mean "do not cache".no-store: no cache may store the response. Use it for sensitive data.publicandprivate:privatelimits storage to the user's browser cache;publicallows shared caches, including for requests withAuthorization.immutable: the content will not change while fresh, so the browser does not even revalidate on reload. Only makes sense when the URL changes along with the content.must-revalidate: once stale, the response is never served without revalidating.max-age=0, must-revalidateis effectivelyno-cache.stale-while-revalidate=N: once stale, the cache may serve the old copy for N more seconds while it fetches a new one in the background.stale-if-error=N: if the server answers with an error (500, 502, 503 or 504), the cache may serve the old copy for up to N seconds.
The most common mistake is assuming no-cache prevents storage. no-store does that.
How does revalidation work?#
When a copy goes stale, the cache does not have to download the whole file again. It asks the server whether anything changed by sending If-None-Match with the ETag it received (or If-Modified-Since with Last-Modified). If nothing changed, the server answers 304 Not Modified with no body, and the copy becomes fresh again. If it changed, the server answers 200 with the new file.
This is the mechanism behind no-cache: the browser always makes a round trip, but most of the time it gets back only a 304 a few bytes long.
Which Cache-Control should you use for each file type?#
| Response type | Header | Why |
|---|---|---|
JS, CSS and images with a hash in the name (app.3f9a1c.js) |
public, max-age=31536000, immutable |
The URL changes when the content changes, so it can stay for a year |
index.html and pages that point to assets |
no-cache |
Always revalidates and picks up the new file names |
| Image or file without a hash in the name | public, max-age=86400 (or less) |
Accepts being stale for a known time |
| API response personalized per user | private, no-cache |
Never goes into a shared cache |
| Sensitive data (token, statement) | no-store |
Must not be stored anywhere |
| Public content that can lag a few minutes | max-age=60, stale-while-revalidate=300 |
Instant response, refreshed in the background |
| Different TTL on the CDN and the browser | max-age=60, s-maxage=3600 |
The CDN keeps it 1 hour, the browser 1 minute |
The logic of the first two rows matters most: HTML never stays cached for long and versioned files stay for a year. Each deploy produces new names, the new HTML points to them and nobody sees an old version. Build tools such as Vite already put the hash in the file name. More in what a static site is.
How do you set it in Node.js (Express)?#
express.static accepts the maxAge and immutable options. With setHeaders you change the header per file. Example with Express 5:
import express from "express";
const app = express();
// Files with a hash in the name: one year, immutable.
app.use(
"/assets",
express.static("dist/assets", { maxAge: "1y", immutable: true }),
);
// Everything else (index.html etc.): always revalidate.
app.use(
express.static("dist", {
setHeaders(res) {
res.setHeader("Cache-Control", "no-cache");
},
}),
);
// API with user data: never in a shared cache.
app.get("/api/me", (req, res) => {
res.set("Cache-Control", "private, no-cache");
res.json({ name: "Ana" });
});
app.listen(process.env.PORT ?? 3000);The first app.use answers with Cache-Control: public, max-age=31536000, immutable. The idea is the same on other servers: in Nginx, use add_header Cache-Control "public, max-age=31536000, immutable"; inside location /assets/.
How do you check that it worked?#
Request only the headers with curl -I and look at cache-control:
curl -sI https://your-domain.com/assets/app.3f9a1c.js | grep -i -E "cache-control|^age|etag"If a CDN is on the path, an age header above zero means the response came from its cache. In the browser, the Network tab of the developer tools shows "(disk cache)" or "(memory cache)" when the local copy was reused. Careful: reloading with F5 or Ctrl+Shift+R sends max-age=0 or no-cache in the request, so test by opening the URL in a new tab.
Which mistakes hurt the most?#
- Long max-age on a file with no hash. The visitor keeps the old CSS until it expires. Use a long lifetime only with versioned URLs.
- Long max-age on HTML. Even with new assets, the browser does not fetch the HTML that points to them.
- Using
no-cacheexpecting it to block caching. That is whatno-storeis for. - Caching a personalized response as
public. A CDN may hand one user's data to another. Useprivate. - Mixing conflicting directives.
private, no-cache, no-store, max-age=0, must-revalidateis equivalent tono-store; pick one intent. - Forgetting the CDN cache after a fix. Changing the header does not clear what was already stored; you need a purge or to wait for expiry.
How to do this on Square Cloud#
On Square Cloud, every plan includes a global CDN that respects the Cache-Control your application sends. This guide's recipe works as is: a hash in asset names with a one-year max-age, no-cache on the HTML, and private or no-store on anything that belongs to one user. You set the headers in code, as in the Express example, deploy, and check with curl -I on the *.squareweb.app subdomain every web application gets with HTTPS.
When a wrong file is already cached, use cache purge, available on the squareweb.app subdomain and on a custom domain (from the Standard plan), from the dashboard or the VS Code extension. It clears the cache for the whole host, so treat it as a one-off fix and let versioned URLs handle everyday deploys.
For files your users upload, Blob Storage lets you set Cache-Control per object and cache rules per folder, without going through your application. See purge in custom domains and the CDN in network.
Network
Custom domains, automatic TLS and a global CDN for your app
FAQs
What is the difference between no-cache and no-store?
no-cache lets a cache store the response but forces it to revalidate with the server before reusing it. no-store forbids any cache from storing the response. For sensitive data, use no-store.
What is the maximum value for max-age?
There is no practical limit you need to memorize: 31536000 seconds (one year) is the usual value for versioned files. Negative or decimal values have undefined behavior.
What is s-maxage?
It is a freshness time that applies only to shared caches such as CDNs and proxies. It overrides max-age there, and browsers ignore it.
Why does the browser not pick up the new version of my file?
Usually the file was served with a long max-age and the URL did not change. Rename the file with a content hash, or lower the max-age of files that change often, such as HTML.
More in Networking and domains
See all 27 articles- What is a load balancer and how does it work?A load balancer spreads incoming requests across several servers. Learn how it works, the main algorithms, layer 4 vs 7, and a working nginx example.
- DNS records explained: A, CNAME, MX, TXTDNS records are the instructions in a zone that tell the internet where a name points. Learn A, AAAA, CNAME, MX, TXT, NS and CAA with examples and dig commands.
- How to point a domain to your server or appTo point a domain to your server or app, create a CNAME (or A) record in your registrar's DNS panel. Steps, how to test it, and common mistakes.
- What is SSL/TLS?SSL/TLS is the protocol that encrypts the connection between a browser and a server and proves the site is genuine. Learn how it works and what a certificate is.