Cache-Control: private vs public (tested)

private and public do not decide whether a response is cached; they decide who may cache it. private means only the user's own browser may store it; CDNs and proxies must not. public lets shared caches store it too, even in cases they normally would not (such as requests with an Authorization header). We counted server hits in Chromium 153 to see what the browser actually does with each.

1. What the browser did (measured)

A Node.js server on localhost counted every request. Chromium 153 navigated to each URL, away, then back to it with a normal navigation (page.goto, not reload). Separately, the page called fetch() twice per URL.

Response headersRequestServer hits for 2 loads
Cache-Control: public, max-age=60navigation1 (second from cache)
Cache-Control: private, max-age=60navigation1 (second from cache)
private, max-age=60 + Set-Cookienavigation1
public, max-age=60 + Set-Cookienavigation1
no Cache-Control, no Last-Modifiednavigation2 (not cached)
no Cache-Control, Last-Modified one day oldnavigation and fetch1 (heuristic freshness)
public, max-age=60fetch1
private, max-age=60fetch1
public, max-age=60fetch with Authorization: Bearer ...1
private, max-age=60fetch with Authorization: Bearer ...1
private, max-age=60navigation then reload()2 (reload revalidates)

So in the browser, private and public behaved identically. The browser is a private cache, so private never stops it; max-age is what makes it reuse the response. If you want the browser not to keep a copy at all, private is the wrong tool: use no-store (see no-cache vs no-store).

Also note the heuristic row: a response with Last-Modified but no Cache-Control was still reused without contacting the server. Leaving the header out does not mean "don't cache".

2. What shared caches do (from the RFC/docs, not measured)

We did not run a CDN or proxy cache for this guide. Per RFC 9111:

3. Which to use

ContentHeader
Per-user page (account, cart, dashboard)Cache-Control: private, max-age=0, must-revalidate or private, no-cache
Per-user API data that can be briefly reusedCache-Control: private, max-age=60
Sensitive data that must not be stored anywhereCache-Control: no-store
Same for everyone, short browser life, longer CDN lifeCache-Control: public, max-age=60, s-maxage=600
Fingerprinted static assetsCache-Control: public, max-age=31536000, immutable

For long-lived assets see stale-while-revalidate and immutable. Pair short or zero max-age with an ETag so revalidation returns a cheap 304. If the response varies by header, also send Vary.

4. Express 5 defaults (measured)

Express 5.2.1, checked with curl -I:

express.static('/dir')                       Cache-Control: public, max-age=0
res.sendFile(file)                           Cache-Control: public, max-age=0
res.sendFile(file, {maxAge: '1h'})           Cache-Control: public, max-age=3600
express.static(dir, {maxAge:'1d', immutable:true})
                                             Cache-Control: public, max-age=86400, immutable
res.json({...})                              (no Cache-Control; ETag only)

All of these also sent an ETag; the static ones sent Last-Modified. Express has no option to switch static files to private, but setting the header yourself before sendFile was kept:

app.get('/me/report', (req, res) => {
  res.set('Cache-Control', 'private, max-age=300');
  res.sendFile('/srv/reports/' + req.user.id + '.pdf');
});
// response: Cache-Control: private, max-age=300

Note that res.json() sent no Cache-Control at all. Per-user JSON behind a CDN should get an explicit private (or no-store).

5. Check your own headers

curl -sI https://example.com/account | grep -i cache-control

Check while logged in (pass your cookie with -H 'Cookie: ...'): per-user pages should show private or no-store, never public.

Frequently asked questions

Does Cache-Control: private stop the browser from caching?

No. In Chromium 153, a response with private, max-age=60 was served from the browser cache on the second navigation and the second fetch; the server saw 1 hit, the same as public. Use no-store to keep it out of the browser cache.

What is the difference between private and public?

Only who may store the response. private: only the end user's browser. public: shared caches like CDNs and proxies too, even for requests with an Authorization header. In the browser we measured no difference.

Does s-maxage work with private?

No. s-maxage only applies to shared caches, and private forbids shared caches from storing the response, so the combination does nothing extra (from RFC 9111, not measured). Browsers ignore s-maxage.

Is a response with no Cache-Control header cached?

It can be. Chromium 153 reused a response that had only a Last-Modified header one day old without contacting the server (heuristic freshness). Without Last-Modified it requested it again.

What Cache-Control does Express send by default?

Express 5.2.1 sent Cache-Control: public, max-age=0 for express.static and res.sendFile, and no Cache-Control for res.json. Set the header yourself with res.set before sending to use private.

Written with AI assistance. Measured on 2026-10-02 with headless Chromium 153.0.8010.12 (Playwright) against a Node.js 24.21.0 http server on localhost:8983, counting server hits per URL, and Express 5.2.1 on localhost:8984 checked with curl 8.14.1. Shared-cache (CDN, nginx proxy_cache) behaviour is quoted from RFC 9111 and vendor docs, not measured. PlainHeaders is our extension; Chromium, Node.js, Express and curl are free.