Every time someone opens a page on your site, their browser asks for dozens of files: the HTML, stylesheets, scripts, fonts, and images. If it had to download every one from scratch on every visit, pages would load more slowly and your server would do far more work than necessary. HTTP caching solves this by letting responses be stored and reused, and the Cache-Control header is the main way you control it.
This guide explains what Cache-Control does, how freshness and validation work, what each important directive means (including the often-confused no-cache and no-store), and which settings suit HTML pages, versioned assets, and personalized content. It draws on MDN's HTTP caching guide and Cache-Control reference, and on RFC 9111, the HTTP caching standard, all linked at the end.
The short answer: give versioned static files a long lifetime with public, max-age=31536000, immutable. Serve HTML with no-cache so it is always revalidated. Mark personalized responses private. And use no-store only for data that must never be kept.
What is HTTP caching?
MDN describes the HTTP cache as something that stores a response associated with a request and reuses the stored response for subsequent requests. Because a cached copy can be used without contacting the origin server, caching reduces server load and improves performance.
RFC 9111 is the formal definition. It specifies HTTP caches and the header fields that control their behavior, and it is an Internet Standard, published as STD 98. Cache-Control is the most important of those header fields.
Private and shared caches
Not all caches are alike. A private cache belongs to a single client, typically a web browser. A shared cache sits between the client and the server and can serve many users. Shared caches include proxy caches and managed caches such as CDNs, reverse proxies, and service workers that you control.
The difference matters for privacy. MDN warns that if personalized content is stored in a cache other than a private cache, other users may be able to retrieve it, which can cause unintentional information leakage. Much of Cache-Control exists to keep that from happening.
Fresh and stale responses
A stored response is either fresh or stale, and the deciding factor is age. MDN explains that age is measured in seconds from when the response was generated. With a lifetime of 604,800 seconds, which is one week, a response younger than a week is fresh and one older than a week is stale.
A fresh response can be reused without asking the server anything. A stale response generally has to be validated first, although RFC 9111 notes that stale responses can still be reused in certain situations, such as when the cache is disconnected from the origin. Because lifetimes are written in seconds, it helps to know the common values.
- 1 hour: 3,600 seconds.
- 1 day: 86,400 seconds.
- 1 week: 604,800 seconds.
- 30 days: 2,592,000 seconds.
- 1 year: 31,536,000 seconds, the usual maximum for long-lived assets.
The Cache-Control directives that matter
Cache-Control takes a comma-separated list of directives. These are the ones you will use most, with their MDN definitions in plain language.
- max-age=N: the response stays fresh until N seconds after it was generated on the origin server. Note that this counts from when the server generated it, not from when your browser received it.
- s-maxage=N: like max-age, but only for shared caches such as CDNs, where it overrides max-age. Private caches ignore it.
- no-cache: the response can be stored, but it must be validated with the origin server before each reuse.
- no-store: no cache of any kind, private or shared, should store the response.
- must-revalidate: once the response is stale, it must be validated with the origin before reuse, instead of being served stale when the origin is unreachable.
- public: the response may be stored in a shared cache, even if the request carried an Authorization header.
- private: the response may be stored only in a private cache, such as the user's browser. MDN says to use it for user-personalized content, especially after login and for cookie-managed sessions.
- immutable: the response will not be updated while it is fresh, so caches can skip unnecessary revalidation requests.
- stale-while-revalidate=N: a cache may reuse a stale response for N seconds while it revalidates in the background.
no-cache vs no-store
These two directives are the most commonly confused, partly because the name no-cache sounds like it means do not cache. MDN is explicit: no-cache does not mean do not cache. It allows caches to store a response but requires them to revalidate it before reuse. RFC 9111 words it as the response must not be used to satisfy another request without being forwarded for validation.
If what you actually want is do not store, then no-store is the directive to use. RFC 9111 says a cache must not store any part of the request or the response. MDN also points out that a do-not-cache requirement usually means one of three things: a privacy concern, where private is the right answer, a need for up-to-date content, where no-cache fits, or dealing with outdated implementations.
- Use no-cache when content changes and visitors should always see the latest version, but efficient revalidation is fine.
- Use no-store for responses that contain highly sensitive data and must not be kept in any cache.
- Use private when the content is personalized but may safely sit in the user's own browser.
Validation: ETag, Last-Modified, and 304
When a response is stale, or marked no-cache, the cache does not have to download it again. It can ask the server whether the stored copy is still current. This is called validation, and it uses conditional requests.
In the ETag approach, the server sends a validator such as ETag: "33a64df5". On the next request the client sends If-None-Match with that value. If nothing has changed, the server replies 304 Not Modified with no body, and the cache reuses its copy. The Last-Modified and If-Modified-Since pair works the same way using dates. A 304 response is tiny compared with the full file, so validation saves bandwidth even when it cannot avoid a round trip.
Cache busting with versioned URLs
A long cache lifetime is only safe if visitors can still receive updates. The standard solution is to change the file's URL whenever its content changes. MDN calls this a common best practice, so the URL unit can be cached for a long time. The version or hash goes in the filename, for example bundle.v123.js or a build-generated name containing a content hash, or sometimes in a query string such as bundle.js?v=123.
Once every version has its own URL, a given URL never changes, and you can mark it as cacheable for a year and immutable. MDN calls this the cache-busting pattern, and describes immutable as avoiding unnecessary conditional requests to the server, such as the ones some browsers make when a user reloads the page.
Recommended settings by content type
MDN's guidance reduces to a few patterns. Match each resource to the one that fits how often it changes and who it is for.
- Versioned or hashed JavaScript, CSS, fonts, and images: Cache-Control: public, max-age=31536000, immutable.
- HTML pages that change: Cache-Control: no-cache, with an ETag or Last-Modified validator so revalidation is cheap.
- Personalized or logged-in pages: Cache-Control: no-cache, private, so only the user's own browser may keep a copy and it is always revalidated.
- Highly sensitive responses that nothing should keep: Cache-Control: no-store.
- Content that can be slightly out of date, such as a public data feed: a short max-age combined with stale-while-revalidate.
stale-while-revalidate: fast now, fresh soon
stale-while-revalidate lets a cache answer immediately with a slightly old copy while it fetches a fresh one in the background. MDN's example is Cache-Control: max-age=604800, stale-while-revalidate=86400. The response is fresh for 7 days. After that it becomes stale, but for the following day the cache may keep serving it while it revalidates in the background.
This suits content where speed matters more than being up to the second, such as a blog index or a public price list. It is a poor fit for anything where a stale answer would be wrong or harmful.
What happens if you set nothing?
Caches do not wait for instructions. MDN notes that HTTP is designed to cache as much as possible, so even without Cache-Control, responses can be stored and reused if certain conditions are met. That is called heuristic caching, and it makes behavior hard to predict.
MDN's advice is clear: basically all responses should explicitly specify a Cache-Control header. Being explicit is the only way to know what browsers and CDNs will actually do.
Common Cache-Control mistakes
Most caching bugs come from a small set of mistakes, and they usually show up as visitors seeing old content or the server being hit more than expected.
- Using no-cache when you meant no-store, or the reverse.
- Giving unversioned files a long max-age, so visitors are stuck with an old stylesheet or script until the lifetime expires.
- Sending no Cache-Control header and relying on heuristic caching.
- Letting a shared cache store personalized pages instead of marking them private.
- Forgetting that max-age counts from when the response was generated, not from when it arrived.
- Serving HTML with a long lifetime, so a new deploy does not reach visitors until old pages expire.
- Omitting validators, so every revalidation downloads the whole file again.
How to check your headers
You can see exactly what your server sends. In Chrome, open DevTools with Ctrl+Shift+J or Command+Option+J, choose the Network tab, reload the page, and click a request to read its response headers. Look for Cache-Control, ETag, and Last-Modified. A 304 status means revalidation succeeded.
When testing changes, remember that your own browser may be serving the old copy. Turn on the Disable cache option in the Network panel while DevTools is open, or test in a private window. From a command line, curl -I followed by a URL prints the response headers without downloading the body.
Practical checklist
- Set an explicit Cache-Control header on every response.
- Use public, max-age=31536000, immutable on versioned static assets.
- Put a version or content hash in the URL of anything with a long lifetime.
- Serve HTML with no-cache and provide an ETag or Last-Modified validator.
- Mark personalized responses private, and use no-store only for data that must never be kept.
- Remember that no-cache means revalidate, not do not cache.
- Test with DevTools and curl -I, with the cache disabled or in a private window.
Research and references
This guide was prepared from the authoritative references below.



