The useful part, up front

What you’ll take away

  • A stored response can be reused without validation only when freshness and the other cache reuse conditions permit it. A stale response normally needs validation or replacement unless serving stale is allowed [1].
  • no-cache permits HTTP cache storage but requires successful validation before reuse. no-store instructs HTTP caches not to store the response; it is not a universal privacy or browser-history guarantee [1][2][3].
  • The private directive restricts storage to a private cache. The public directive permits shared caching in circumstances where it might otherwise be disallowed, subject to the remaining protocol rules [1][3].
  • Versioned asset URLs can support long freshness lifetimes; an HTML document that must reflect changes can instead use validation. The appropriate policy depends on the resource and its requirements [2].
  • Cloudflare settings and product behavior can affect Origin Cache Control and Vary handling. Check the documented behavior for the configuration in use [3].
01

Why a cache exists and what it stores

An HTTP cache stores a response associated with a request and reuses that stored response for later requests [2]. The main benefit is speed: if the cache is close to the person asking, the answer can arrive without travelling all the way to the origin server [2]. It also reduces work for the origin, because the server does not need to parse the request, restore a session, query a database, or render a template for a reusable response [2].

A cache does not simply keep every response forever. It chooses a stored response using a cache key, which at minimum includes the request method and the target URI [1]. Many common caches focus on GET responses and use the URI as the key [1]. A typical stored result is a 200 response to a GET, although redirects, 404 responses, and partial 206 responses can also be stored when the method definition allows it [1].

Caching is optional in HTTP, but reuse is the expected default when no rule or local configuration prevents it [1]. The standard therefore concentrates on stopping a cache from storing something that should not be stored, or reusing something inappropriately, rather than forcing every cache to store every response [1]. For a site owner, that means headers are instructions to be respected, not guarantees that every intermediary will behave identically [1][2].

The examples below assume ordinary GET requests and responses that are otherwise eligible for caching. A freshness lifetime is only one part of the decision: the response policy, the request, and any required validation still matter. These are explanatory scenarios, not results from a test of your browser or CDN.

Private versus shared cache storage locations. Private caches serve one client; shared caches may serve many users and need stricter controls.
Private caches serve one client; shared caches may serve many users and need stricter controls. · Ameer Journal — original explanatory diagramOriginal editorial illustration; no third-party artwork reused.
02

Private caches, shared caches, and who may see a copy

There are two main kinds of HTTP cache: private caches and shared caches [2]. A private cache is tied to one client, typically a browser cache [2]. Because its stored responses are not shared with other clients, a private cache can store a personalised response for that user [2]. A shared cache sits between client and server and can store responses that may be shared among users [2].

Shared caches include proxy caches and managed caches [2]. A proxy cache may be run for access control or to reduce traffic out of a network, and it is usually not managed by the site developer, so headers must control it [2]. Outdated proxy implementations that do not properly understand current caching rules have caused problems for developers in the past [2]. A managed cache, such as a CDN cache, is usually controlled through Cache-Control headers plus product configuration files or dashboards [2].

The practical question is not only whether something can be cached, but where. A response intended for one signed-in user should not be stored by a shared cache that could serve it to someone else [1][2]. The private directive indicates a response is intended for a single user, such as a browser cache, and must not be stored by a shared cache [3]. If you are worried about a shared cache in particular, adding private can help prevent unintended shared storage [2].

no-cache versus no-store semantics. no-cache allows storage with mandatory revalidation; no-store forbids storage entirely.
no-cache allows storage with mandatory revalidation; no-store forbids storage entirely. · Ameer Journal — original explanatory diagramOriginal editorial illustration; no third-party artwork reused.
03

Freshness: max-age, Expires, Age, and heuristic guessing

Freshness answers a simple question: can this stored copy be used right now without asking the origin again? A response's freshness lifetime is the time between its generation by the origin and its expiration time [1]. An explicit expiration time is set by the origin, while a heuristic expiration time is assigned by the cache when no explicit time is available [1]. Freshness can permit direct reuse, provided the other applicable cache rules allow it; no-cache can still require validation [1][2].

The most common explicit control is Cache-Control: max-age, which gives a lifetime in seconds [2][3]. For example, max-age=604800 means the response is treated as fresh for that many seconds after it was served [2][3]. The Age header reports how old a stored response already is, so a cache can compare age against max-age [2][3]. Expires gives an absolute date instead of a duration, but max-age is preferred when both are present [2].

If no Cache-Control is given, HTTP may still store and reuse a response under heuristic caching when conditions are met [2]. That default is not the same as do not cache [2]. To avoid heuristic guessing, it is preferable to give every response an explicit Cache-Control header [2]. Heuristics must not be used when an explicit expiration time is present, and they are limited to certain heuristically cacheable status codes or responses explicitly marked cacheable [1].

Validation request and response sequence. When usable validators are available, a conditional request can confirm the stored response or return a replacement.
When usable validators are available, a conditional request can confirm the stored response or return a replacement. · Ameer Journal — original explanatory diagramOriginal editorial illustration; no third-party artwork reused.
04

A hypothetical cache-age timeline

The following timeline is an original hypothetical example, created only to show the arithmetic; it is not a measurement from any real site. Assume, hypothetically, that an origin returns a page at 10:00 with Cache-Control: max-age=3600 and an ETag [2]. That means the stored copy is fresh for 3600 seconds, or one hour, after it was served [3]. At 10:20, a visitor requests the same URL. The cache age is about 1200 seconds, which is less than 3600, so the copy is still fresh and can be reused without contacting the origin [1][2].

At 11:10, the age is about 4200 seconds, which is greater than 3600, so the copy is stale [3]. Stale does not mean deleted. The cache can ask the origin whether the stored copy is still valid, using validation [2]. If the origin says the content has not changed, the cache can freshen the stored copy and reuse it [1][2]. If the content has changed, the origin sends a new response and the cache replaces the old one [1].

This example shows why max-age and validation work together. Freshness avoids origin contact entirely, while validation avoids sending the full body again when nothing has changed [1][2]. It also shows the risk of a long max-age: during the fresh period, the cache will reuse the stored copy without checking the origin [1]. Choose the lifetime according to how quickly the content is allowed to become outdated, not according to the longest value that looks attractive [2].

Hypothetical cache-age timeline. Hypothetical GET example: max-age=3600, usable validators, no earlier validation requirement, and no permission to serve stale.
Hypothetical GET example: max-age=3600, usable validators, no earlier validation requirement, and no permission to serve stale. · Ameer Journal — original explanatory diagramOriginal editorial illustration; no third-party artwork reused.
05

Validation with ETag, Last-Modified, and conditional requests

When a stored copy is stale, the cache does not have to throw it away. It can validate it by asking the origin whether the copy is still current [1][2]. Validation is done with a conditional request that includes If-Modified-Since or If-None-Match [2]. If the content has not changed, the origin can reply with 304 Not Modified, and the cache can reuse and freshen its stored copy instead of downloading the whole body again [2].

Two validators are commonly used. Last-Modified is a timestamp that can be sent back in If-Modified-Since [1]. An ETag is an entity tag that can be sent back in If-None-Match [1]. In many cases both validators are sent during validation, even when the ETag is stronger, so older intermediaries that do not understand entity tags can still respond appropriately [1]. If both If-Modified-Since and If-None-Match are present, If-None-Match takes precedence [1][2].

For site owners, the lesson is to send useful validators on cacheable responses. Providing both ETag and Last-Modified is generally better for the wider HTTP ecosystem, because Last-Modified is also used by content systems and crawlers for other purposes [2]. Without validators, validation may be impossible and the cache may have to download a fresh full response even when the content is unchanged [1][2].

Cache key and Vary partitioning. The method, URI and Vary match help select a stored response; freshness and other reuse rules must also permit its use.
The method, URI and Vary match help select a stored response; freshness and other reuse rules must also permit its use. · Ameer Journal — original explanatory diagramOriginal editorial illustration; no third-party artwork reused.
06

no-cache means revalidate, not do not store

The most misunderstood directive is no-cache. Despite its name, no-cache does not prevent a response from being stored [2]. Instead, it prevents reuse of the stored response without revalidation [2]. In other words, the cache may keep a copy, but before using it for a later request it must check with the origin first [2][3].

That makes no-cache useful for main HTML pages that change often but are not sensitive [3]. The browser or shared cache can store the page, but each reuse requires a successful validation with the origin, so the content stays current [3]. If the origin confirms nothing changed, the cache can reuse the stored body after a lightweight 304 response [2]. If the origin is unreachable or validation fails, the stored copy must not simply be reused as if it were fresh [1][3].

A common pattern is Cache-Control: no-cache on the main document, combined with long max-age values on versioned static assets [2]. The HTML is checked every time, while the JavaScript and CSS files it references can be reused for a long time because their URLs change when their contents change [2]. This split gives freshness where it matters and speed where it is safe [2].

07

no-store, private, and public: controlling where copies live

Use no-store when you want to prohibit HTTP caches from storing the response [2][3]. This is a different request from allowing a stored copy but requiring a check before reuse. In this example, removing the option to reuse a stored response means the request needs another response. This is an explanatory consequence of the assumed scenario. Do not treat an HTTP cache setting as a complete privacy plan: separately review the application’s handling of sensitive information. The examples here make no universal claim about browser history restoration or application storage.

Private and public control which caches may store a response. Private means the response is intended for a single user, such as a browser cache, and must not be stored by a shared cache [3]. Public means any cache may store the response, even if it would normally be non-cacheable or only cacheable privately [3]. Public is mainly needed when a response must be stored despite an Authorization header, because otherwise a response with max-age will already be stored by a shared cache [2].

Sensitive responses need more than a vague intention not to cache. Set-Cookie alone does not stop caching, so servers should emit appropriate Cache-Control fields for responses they want to control [1]. For a signed-in page, a common combination is no-cache with private, so the browser may store it but a shared cache must not, and reuse still requires validation [2]. For sensitive responses that HTTP caches must not retain, no-store expresses that restriction [2][3].

08

Shared-cache safety, Vary, and authenticated responses

Shared caches are powerful because one stored copy can serve many visitors, but that is also why they are risky for personalised content [2]. A shared cache must not use a cached response to an authenticated request for a later request unless the response explicitly allows shared storage [1]. If a page shows different content to different signed-in users, the default assumption should be that a shared cache must not store it [1][2].

The Vary header adds another dimension. It lists request headers that affect which response is correct, such as language or encoding [1]. If a stored response contains Vary, the cache must not use it without revalidation unless the new request's nominated headers match the original request's headers [1]. Without Vary, a cache might serve a response generated for one kind of request to a different kind of request that happens to use the same URL [1].

Cloudflare adds a provider-specific caveat: by default, Cloudflare does not consider Vary values in caching decisions [3]. Vary is respected only when the Cache Rules Vary setting is configured, when Vary for images is configured, or when the header is Vary: Accept-Encoding [3]. That means a design that relies on Vary for correctness may behave differently on Cloudflare than the protocol alone would suggest [1][3]. Always check the actual CDN documentation for your configuration [3].

09

Versioned assets, immutable, and cache busting

The resources that work best with long caching are static files whose contents never change [2]. For resources that do change, a common practice is to change the URL each time the content changes, so the new URL can be cached for a long period [2]. Because a cache distinguishes resources by URL, changing the URL means the old stored copy will not be reused for the new content [2]. This technique is called cache busting [2].

A typical setup gives a versioned file such as bundle.v123.js a long lifetime with Cache-Control: public, max-age=31536000, immutable [2]. The immutable directive tells clients the body does not change over time and that they should not send a conditional revalidation request even on an explicit refresh [3]. Cloudflare notes that immutable has no effect on public caches like Cloudflare, but does change browser behaviour [3].

Cache busting has a cost: every content change creates a new URL, so old copies remain in caches until they expire or are evicted [2]. That is usually acceptable for assets, but it means the main HTML page must not be cached for a long time, or visitors will keep requesting old asset URLs [2]. Pairing a revalidated HTML page with long-lived versioned assets is the standard compromise [2].

10

Failure cases, edge cases, and a practical checklist

Several failure cases recur. Conflicting directives, such as max-age together with no-cache, should be resolved in favour of the most restrictive directive [1]. Invalid freshness information, such as a non-integer max-age, should encourage treating the response as stale [1]. Duplicate values for a directive may lead a cache to use the first occurrence or treat the response as stale [1]. These edge cases are reasons to keep headers simple and consistent [1].

Another failure case is relying on a CDN to follow the protocol without checking its settings. Cloudflare's Origin Cache Control feature, when enabled, makes Cloudflare strictly respect origin Cache-Control directives [3]. Free, Pro, and Business customers have it enabled by default, while Enterprise customers can enable or disable it [3]. When it is disabled, Cloudflare may not cache where the origin expected caching, or may ignore directives such as must-revalidate and serve stale content [3]. Cache Rules can also augment or override origin headers [3].

Use this checklist before shipping. Give every response an explicit Cache-Control header to avoid heuristic caching [2]. Decide for each resource whether it may be stored privately, shared, or nowhere [2][3]. Set max-age according to acceptable staleness, add validators for revalidation, and use no-cache for frequently changing main pages [2]. Version static assets and add immutable where appropriate [2][3]. Finally, test the actual CDN behaviour, including Vary handling and Origin Cache Control settings, rather than assuming the protocol alone describes it [3].

Follow the evidence

Sources & further reading

Explore the references behind this guide. External pages may change after our review.

  1. RFC 9111: HTTP Caching
    RFC Editor / IETF · Accessed 6 Nov 2026
  2. HTTP caching - HTTP | MDN
    MDN / Mozilla · Accessed 6 Nov 2026
  3. Origin Cache Control · Cloudflare Cache (CDN) docs
    Cloudflare · Accessed 6 Nov 2026

Article updated 6 Nov 2026.

Found something we should revisit?

Good explanations get better with thoughtful feedback.

Suggest a correction