How to optimize caching of images on a webpage - http

I have a website that contains pages with many small images. The images are set to cache, with the headers containing:
Expires "Thu, 31 Dec 2037 23:55:55 GMT"
Cache-Control "public, max-age=315360000"
When someone loads a page, however, it seems that we are still forced to send a 304 response for each image--better than sending the whole image, but still takes some time. Of course, this sort of caching is up to the browser, but is it possible to suggest to the browser that it use the cached images without any request at all?

If you have many small images on a page, consider making a CSS sprite with all the images - that will reduce the number of requests a lot. A List Apart explains the concept.

Take a look at RFC 2616, Part of the HTTP/1.1 Protocol:
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
You'll see lots of options to play with. Primarily intended for proxies, not Browsers. You can't really force the browser to totally stop Modified-Since-Requests.
Especially older proxies might ignore your Cache-Control hints, see the mentioned paragraph on the aforementioned website:
Note that HTTP/1.0 caches might not implement Cache-Control and
might only implement Pragma: no-cache (see section 14.32).
If you are really concerned about such short requests, take a look if HTTP-keepalive feature in your server is enabled (which has sideeffects on it's own, of course).

Related

Understand Request HTTP Cache-Control [duplicate]

The header Cache-Control: max-age=0 implies that the content is considered stale (and must be re-fetched) immediately, which is in effect the same thing as Cache-Control: no-cache.
I had this same question, and found some info in my searches (your question came up as one of the results). Here's what I determined...
There are two sides to the Cache-Control header. One side is where it can be sent by the web server (aka. "origin server"). The other side is where it can be sent by the browser (aka. "user agent").
When sent by the origin server
I believe max-age=0 simply tells caches (and user agents) the response is stale from the get-go and so they SHOULD revalidate the response (eg. with the If-Not-Modified header) before using a cached copy, whereas, no-cache tells them they MUST revalidate before using a cached copy. From 14.9.1 What is Cacheable:
no-cache
...a cache MUST NOT use the response
to satisfy a subsequent request
without successful revalidation with
the origin server. This allows an
origin server to prevent caching even
by caches that have been configured to
return stale responses to client
requests.
In other words, caches may sometimes choose to use a stale response (although I believe they have to then add a Warning header), but no-cache says they're not allowed to use a stale response no matter what. Maybe you'd want the SHOULD-revalidate behavior when baseball stats are generated in a page, but you'd want the MUST-revalidate behavior when you've generated the response to an e-commerce purchase.
Although you're correct in your comment when you say no-cache is not supposed to prevent storage, it might actually be another difference when using no-cache. I came across a page, Cache Control Directives Demystified, that says (I can't vouch for its correctness):
In practice, IE and Firefox have
started treating the no-cache
directive as if it instructs the
browser not to even cache the page.
We started observing this behavior
about a year ago. We suspect that
this change was prompted by the
widespread (and incorrect) use of this
directive to prevent caching.
...
Notice that of late, "cache-control:
no-cache" has also started behaving
like the "no-store" directive.
As an aside, it appears to me that Cache-Control: max-age=0, must-revalidate should basically mean the same thing as Cache-Control: no-cache. So maybe that's a way to get the MUST-revalidate behavior of no-cache, while avoiding the apparent migration of no-cache to doing the same thing as no-store (ie. no caching whatsoever)?
When sent by the user agent
I believe shahkalpesh's answer applies to the user agent side. You can also look at 13.2.6 Disambiguating Multiple Responses.
If a user agent sends a request with Cache-Control: max-age=0 (aka. "end-to-end revalidation"), then each cache along the way will revalidate its cache entry (eg. with the If-Not-Modified header) all the way to the origin server. If the reply is then 304 (Not Modified), the cached entity can be used.
On the other hand, sending a request with Cache-Control: no-cache (aka. "end-to-end reload") doesn't revalidate and the server MUST NOT use a cached copy when responding.
max-age=0
This is equivalent to clicking Refresh, which means, give me the latest copy unless I already have the latest copy.
no-cache
This is holding Shift while clicking Refresh, which means, just redo everything no matter what.
Old question now, but if anyone else comes across this through a search as I did, it appears that IE9 will be making use of this to configure the behaviour of resources when using the back and forward buttons. When max-age=0 is used, the browser will use the last version when viewing a resource on a back/forward press. If no-cache is used, the resource will be refetched.
Further details about IE9 caching can be seen on this msdn caching blog post.
In my recent tests with IE8 and Firefox 3.5, it seems that both are RFC-compliant. However, they differ in their "friendliness" to the origin server. IE8 treats no-cache responses with the same semantics as max-age=0,must-revalidate. Firefox 3.5, however, seems to treat no-cache as equivalent to no-store, which sucks for performance and bandwidth usage.
Squid Cache, by default, seems to never store anything with a no-cache header, just like Firefox.
My advice would be to set public,max-age=0 for non-sensitive resources you want to have checked for freshness on every request, but still allow the performance and bandwidth benefits of caching. For per-user items with the same consideration, use private,max-age=0.
I would avoid the use of no-cache entirely, as it seems it has been bastardized by some browsers and popular caches to the functional equivalent of no-store.
Additionally, do not emulate Akamai and Limelight. While they essentially run massive caching arrays as their primary business, and should be experts, they actually have a vested interest in causing more data to be downloaded from their networks. Google might not be a good choice for emulation, either. They seem to use max-age=0 or no-cache randomly depending on the resource.
max-age
When an intermediate cache is forced, by means of a max-age=0 directive, to revalidate
its own cache entry, and the client has supplied its own validator in the request, the
supplied validator might differ from the validator currently stored with the cache entry.
In this case, the cache MAY use either validator in making its own request without
affecting semantic transparency.
However, the choice of validator might affect performance. The best approach is for the
intermediate cache to use its own validator when making its request. If the server replies
with 304 (Not Modified), then the cache can return its now validated copy to the client
with a 200 (OK) response. If the server replies with a new entity and cache validator,
however, the intermediate cache can compare the returned validator with the one provided in
the client's request, using the strong comparison function. If the client's validator is
equal to the origin server's, then the intermediate cache simply returns 304 (Not
Modified). Otherwise, it returns the new entity with a 200 (OK) response.
If a request includes the no-cache directive, it SHOULD NOT include min-fresh,
max-stale, or max-age.
courtesy: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
Don't accept this as answer - I will have to read it to understand the true usage of it :)
I'm hardly a caching expert, but Mark Nottingham is. Here are his caching docs. He also has excellent links in the References section.
Based on my reading of those docs, it looks like max-age=0 could allow the cache to send a cached response to requests that came in at the "same time" where "same time" means close enough together they look simultaneous to the cache, but no-cache would not.
By the way, it's worth noting that some mobile devices, particularly Apple products like iPhone/iPad completely ignore headers like no-cache, no-store, Expires: 0, or whatever else you may try to force them to not re-use expired form pages.
This has caused us no end of headaches as we try to get the issue of a user's iPad say, being left asleep on a page they have reached through a form process, say step 2 of 3, and then the device totally ignores the store/cache directives, and as far as I can tell, simply takes what is a virtual snapshot of the page from its last state, that is, ignoring what it was told explicitly, and, not only that, taking a page that should not be stored, and storing it without actually checking it again, which leads to all kinds of strange Session issues, among other things.
I'm just adding this in case someone comes along and can't figure out why they are getting session errors with particularly iphones and ipads, which seem by far to be the worst offenders in this area.
I've done fairly extensive debugger testing with this issue, and this is my conclusion, the devices ignore these directives completely.
Even in regular use, I've found that some mobiles also totally fail to check for new versions via say, Expires: 0 then checking last modified dates to determine if it should get a new one.
It simply doesn't happen, so what I was forced to do was add query strings to the css/js files I needed to force updates on, which tricks the stupid mobile devices into thinking it's a file it does not have, like: my.css?v=1, then v=2 for a css/js update. This largely works.
User browsers also, by the way, if left to their defaults, as of 2016, as I continuously discover (we do a LOT of changes and updates to our site) also fail to check for last modified dates on such files, but the query string method fixes that issue. This is something I've noticed with clients and office people who tend to use basic normal user defaults on their browsers, and have no awareness of caching issues with css/js etc, almost invariably fail to get the new css/js on change, which means the defaults for their browsers, mostly MSIE / Firefox, are not doing what they are told to do, they ignore changes and ignore last modified dates and do not validate, even with Expires: 0 set explicitly.
This was a good thread with a lot of good technical information, but it's also important to note how bad the support for this stuff is in particularly mobile devices. Every few months I have to add more layers of protection against their failure to follow the header commands they receive, or to properly interpet those commands.
This is answered directly in the MDN docs about cache control:
Most HTTP/1.0 caches don't support no-cache directives, so
historically max-age=0 was used as a workaround. But only
max-age=0 could cause a stale response to be reused when caches
disconnected from the origin server. must-revalidate addresses that.
That's why the example below is equivalent to no-cache.
Cache-Control: max-age=0, must-revalidate
But for now, you can simply use no-cache instead.
And also in the MDN docs about cache validation:
It is often stated that the combination of max-age=0 and
must-revalidate has the same meaning as no-cache.
Cache-Control: max-age=0, must-revalidate
max-age=0 means that the
response is immediately stale, and must-revalidate means that it must
not be reused without revalidation once it is stale — so in
combination, the semantics seem to be the same as no-cache.
However, that usage of max-age=0 is a remnant of the fact that many
implementations prior to HTTP/1.1 were unable to handle the no-cache
directive — and so to deal with that limitation, max-age=0 was used as
a workaround.
But now that HTTP/1.1-conformant servers are widely deployed, there's
no reason to ever use that max-age=0-and-must-revalidate combination —
you should instead just use no-cache.
One thing that (surprisingly) hasn't been mentioned is that a request can explicitly indicate that it will accept stale data, using the max-stale directive. In that case, if the server responded with max-age=0, the cache would merely consider the response stale, and would be free to use it to satisfy the client's request [which asked for potentially-stale data]. By contrast, if the server sends no-cache that really does trump any request by the client (with max-stale) for stale data, as the cache MUST revalidate.

Is it a good idea to always send Cache-Control: must-revalidate along with Last-Modified

I just read this article and in the author's discussion of the Last-Modified HTTP header he recommends that Cache-Control: must-revalidated also be sent. He states:
What if server doesn’t send
Cache-Control: must-revalidate? Then
modern browsers look at profile
setting or decide on their own whether
to send conditional request. So we
better to send Cache-Control to make
sure that browser sends conditional
request.
So, my question is, what's wrong with letting the browser decide? And why would we want to blindly override a browser's profile setting? I understand that there may be situations when we want to force revalidation but should it always be done?
It really depends on your usage.
I'm "with you" for the most part, because on the one hand you're shooting yourself in the foot since you're basically throwing out the advantage of avoiding a round-trip in the first place (caching should avoid a round-trip if possible, then avoid sending content if possible, and then give up and send content, and this author is removing the first gateway if you force the browser to conditionally check before serving from its cache).
On the other hand, maybe you hate funny cache invalidation strings in your code, i.e "main.css?v=2" and want the browser to ask, so you have the opportunity to check a cached ETag on your server and invalidate. That seems like kind of a crummy reason, but I can see it being useful for CMS systems or when you don't have control of the URI.

HTTP headers for the most permanent caching possible

For example: a stable release of jQuery will never change until the next version. When that happens, the URL will change.
Also, images like a website logo can be cached and when it changes I simply change the URL that is used to call it.
The header I know of is
Expires: Tue, 01 Feb 2050 00:00:00 GMT
I believe there are one or more additional headers I can use to add to the caching for proxies and maybe there is something I don't know about.
Are there any other headers I should know about?
Granted the cache will get deleted beyond my control. But I want to cache as much as possible.
Also this does not cover CSS/JavaScript minify/compile and it also does not count image compression or content compression such as gzip.
Expires is HTTP/1.0 and HTTP/1.1 introduced the more versatile Cache-Control where you can not just specify an expiration date but also cacheability and revalidation.
I recommend you to read Mark Nottingham’s Caching Tutorial.

What is the function of the "Vary: Accept" HTTP header?

I use PHP to generate dynamic Web pages. As stated on the following tutorial (see link below), the MIME type of XHTML documents should be "application/xhtml+xml" when $_SERVER['HTTP_ACCEPT'] allows it. Since you can serve the same page with 2 different MIMEs ("application/xhtml+xml" and "text/html") you should set the "Vary" HTTP header to "Accept". This will help the cache on proxies.
Link:
http://keystonewebsites.com/articles/mime_type.php
Now I'm not sure of the implication of:
header('Vary: Accept');
I'm not really sure of what 'Vary: Accept' will precisely do...
The only explanation I found is:
After the Content-Type header, a Vary
header is sent to (if I understand it
correctly) tell intermediate caches,
like proxy servers, that the content
type of the document varies depending
on the capabilities of the client
which requests the document.
http://www.456bereastreet.com/archive/200408/content_negotiation/
Anyone can give me a "real" explanation of this header (with that value). I think I understand things like:
Vary: Accept-Encoding
where the cache on proxies could be based on the encoding of the page served, but I don't understand:
Vary: Accept
The cache-control header is the primary mechanism for an HTTP server to tell a caching proxy the "freshness" of a response. (i.e., how/if long to store the response in the cache)
In some situations, cache-control directives are insufficient. A discussion from the HTTP working group is archived here, describing a page that changes only with language. This is not the correct use case for the vary header, but the context is valuable for our discussion. (Although I believe the Vary header would solve the problem in that case, there is a Better Way.) From that page:
Vary is strictly for those cases where it's hopeless or excessively complicated for a proxy to replicate what the server would do.
RFC2616 "Header-Field Definitions" describes the header usage from the server perspective, RFC2616 "Caching Negotiated Responses" from a caching proxy perspective. It's intended to specify a set of HTTP request headers that determine uniqueness of a request.
A contrived example:
Your HTTP server has a large landing page. You have two slightly different pages with the same URL, depending if the user has been there before. You distinguish between requests and a user's "visit count" based on Cookies. But -- since your server's landing page is so large, you want intermediary proxies to cache the response if possible.
The URL, Last-Modified and Cache-Control headers are insufficient to give this insight to a caching proxy, but if you add Vary: Cookie, the cache engine will add the Cookie header to its caching decisions.
Finally, for small traffic, dynamic web sites -- I have always found the simple Cache-Control: no-cache, no-store and Pragma: no-cache sufficient.
Edit -- to more precisely answer your question: the HTTP request header 'Accept' defines the Content-Types a client can process. If you have two copies of the same content at the same URL, differing only in Content-Type, then using Vary: Accept could be appropriate.
Update 11 Sep 12:
I'm including a couple links that have appeared in the comments since this comment was originally posted. They're both excellent resources for real-world examples (and problems) with Vary: Accept; Iif you're reading this answer you need to read those links as well.
The first, from the outstanding EricLaw, on Internet Explorer's behavior with the Vary header and some of the challenges it presents to developers: Vary Header Prevents Caching in IE. In short, IE (pre IE9) does not cache any content that uses the Vary header because the request cache does not include HTTP Request headers. EricLaw (Eric Lawrence in the real world) is a Program Manager on the IE team.
The second is from Eran Medan, and is an on-going discussion of Vary-related unexpected behavior in Chrome: Backing doesn't handle Vary header correctly. It's related to IE's behavior, except the Chrome devs took a different approach -- though it doesn't appear to have been a deliberate choice.
Vary: Accept simply says that the response was generated based on the Accept header in the request. A request with a different Accept header might get a different response.
(You can see that the linked PHP code looks at $HTTP_ACCEPT. That's the value of the Accept request header.)
To HTTP caches, this means that the response must be cached with extra care. It is only going to be a valid match for later requests with exactly the same Accept header.
Now this only matters if the page is cacheable in the first place. By default, PHP pages aren't. A PHP page can mark the output as cacheable by sending certain headers (Expires, for example). But whether and how to do that is a different question.
This google webmaster video has a very good explanation about HTTP Vary header.
There are actually a significant number of new features coming soon (and already in Chrome) that make the Vary header extremely useful. For example, consider Client Hinting. When used in connection with images, for example, client hinting allows a server to optimize resources such as images depending on:
Image Width
Viewport Width
Type of encoding supported by browser (think WebP)
Downlink (essentially network speed)
So a server which supports those features would set the Vary header to indicate that.
Chrome advertises WebP support by setting "image/webp" as part of the Vary header for each request. So a server might rewrite an image as WebP if the browser supports it, so the proxy would need to check the header so as to not cache a WebP image and then serve it to a browser that doesn't support WebP. Obviously, if your server doesn't do that, it wouldn't matter. So since the server's response varies on the Accept request header, the response must include that so as not to confuse proxies:
Vary: Accept
Another example might be image width. On a mobile browser the Width header might be quite small for a responsive image, in comparison with what it would be if viewed from a desktop browser. So in that case Width would be added to the the Vary header is essential for proxy to not cache the small mobile version and serve it to desktop browsers, or vice versa. In that case, the header might include:
Vary: Accept, Width
Or in the case that a server supported all of the client hinting specs, the header would be something like:
Vary: Accept, DPR, Width, Save-Data, Downlink

What's the difference between Cache-Control: max-age=0 and no-cache?

The header Cache-Control: max-age=0 implies that the content is considered stale (and must be re-fetched) immediately, which is in effect the same thing as Cache-Control: no-cache.
I had this same question, and found some info in my searches (your question came up as one of the results). Here's what I determined...
There are two sides to the Cache-Control header. One side is where it can be sent by the web server (aka. "origin server"). The other side is where it can be sent by the browser (aka. "user agent").
When sent by the origin server
I believe max-age=0 simply tells caches (and user agents) the response is stale from the get-go and so they SHOULD revalidate the response (eg. with the If-Not-Modified header) before using a cached copy, whereas, no-cache tells them they MUST revalidate before using a cached copy. From 14.9.1 What is Cacheable:
no-cache
...a cache MUST NOT use the response
to satisfy a subsequent request
without successful revalidation with
the origin server. This allows an
origin server to prevent caching even
by caches that have been configured to
return stale responses to client
requests.
In other words, caches may sometimes choose to use a stale response (although I believe they have to then add a Warning header), but no-cache says they're not allowed to use a stale response no matter what. Maybe you'd want the SHOULD-revalidate behavior when baseball stats are generated in a page, but you'd want the MUST-revalidate behavior when you've generated the response to an e-commerce purchase.
Although you're correct in your comment when you say no-cache is not supposed to prevent storage, it might actually be another difference when using no-cache. I came across a page, Cache Control Directives Demystified, that says (I can't vouch for its correctness):
In practice, IE and Firefox have
started treating the no-cache
directive as if it instructs the
browser not to even cache the page.
We started observing this behavior
about a year ago. We suspect that
this change was prompted by the
widespread (and incorrect) use of this
directive to prevent caching.
...
Notice that of late, "cache-control:
no-cache" has also started behaving
like the "no-store" directive.
As an aside, it appears to me that Cache-Control: max-age=0, must-revalidate should basically mean the same thing as Cache-Control: no-cache. So maybe that's a way to get the MUST-revalidate behavior of no-cache, while avoiding the apparent migration of no-cache to doing the same thing as no-store (ie. no caching whatsoever)?
When sent by the user agent
I believe shahkalpesh's answer applies to the user agent side. You can also look at 13.2.6 Disambiguating Multiple Responses.
If a user agent sends a request with Cache-Control: max-age=0 (aka. "end-to-end revalidation"), then each cache along the way will revalidate its cache entry (eg. with the If-Not-Modified header) all the way to the origin server. If the reply is then 304 (Not Modified), the cached entity can be used.
On the other hand, sending a request with Cache-Control: no-cache (aka. "end-to-end reload") doesn't revalidate and the server MUST NOT use a cached copy when responding.
max-age=0
This is equivalent to clicking Refresh, which means, give me the latest copy unless I already have the latest copy.
no-cache
This is holding Shift while clicking Refresh, which means, just redo everything no matter what.
Old question now, but if anyone else comes across this through a search as I did, it appears that IE9 will be making use of this to configure the behaviour of resources when using the back and forward buttons. When max-age=0 is used, the browser will use the last version when viewing a resource on a back/forward press. If no-cache is used, the resource will be refetched.
Further details about IE9 caching can be seen on this msdn caching blog post.
In my recent tests with IE8 and Firefox 3.5, it seems that both are RFC-compliant. However, they differ in their "friendliness" to the origin server. IE8 treats no-cache responses with the same semantics as max-age=0,must-revalidate. Firefox 3.5, however, seems to treat no-cache as equivalent to no-store, which sucks for performance and bandwidth usage.
Squid Cache, by default, seems to never store anything with a no-cache header, just like Firefox.
My advice would be to set public,max-age=0 for non-sensitive resources you want to have checked for freshness on every request, but still allow the performance and bandwidth benefits of caching. For per-user items with the same consideration, use private,max-age=0.
I would avoid the use of no-cache entirely, as it seems it has been bastardized by some browsers and popular caches to the functional equivalent of no-store.
Additionally, do not emulate Akamai and Limelight. While they essentially run massive caching arrays as their primary business, and should be experts, they actually have a vested interest in causing more data to be downloaded from their networks. Google might not be a good choice for emulation, either. They seem to use max-age=0 or no-cache randomly depending on the resource.
max-age
When an intermediate cache is forced, by means of a max-age=0 directive, to revalidate
its own cache entry, and the client has supplied its own validator in the request, the
supplied validator might differ from the validator currently stored with the cache entry.
In this case, the cache MAY use either validator in making its own request without
affecting semantic transparency.
However, the choice of validator might affect performance. The best approach is for the
intermediate cache to use its own validator when making its request. If the server replies
with 304 (Not Modified), then the cache can return its now validated copy to the client
with a 200 (OK) response. If the server replies with a new entity and cache validator,
however, the intermediate cache can compare the returned validator with the one provided in
the client's request, using the strong comparison function. If the client's validator is
equal to the origin server's, then the intermediate cache simply returns 304 (Not
Modified). Otherwise, it returns the new entity with a 200 (OK) response.
If a request includes the no-cache directive, it SHOULD NOT include min-fresh,
max-stale, or max-age.
courtesy: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
Don't accept this as answer - I will have to read it to understand the true usage of it :)
I'm hardly a caching expert, but Mark Nottingham is. Here are his caching docs. He also has excellent links in the References section.
Based on my reading of those docs, it looks like max-age=0 could allow the cache to send a cached response to requests that came in at the "same time" where "same time" means close enough together they look simultaneous to the cache, but no-cache would not.
By the way, it's worth noting that some mobile devices, particularly Apple products like iPhone/iPad completely ignore headers like no-cache, no-store, Expires: 0, or whatever else you may try to force them to not re-use expired form pages.
This has caused us no end of headaches as we try to get the issue of a user's iPad say, being left asleep on a page they have reached through a form process, say step 2 of 3, and then the device totally ignores the store/cache directives, and as far as I can tell, simply takes what is a virtual snapshot of the page from its last state, that is, ignoring what it was told explicitly, and, not only that, taking a page that should not be stored, and storing it without actually checking it again, which leads to all kinds of strange Session issues, among other things.
I'm just adding this in case someone comes along and can't figure out why they are getting session errors with particularly iphones and ipads, which seem by far to be the worst offenders in this area.
I've done fairly extensive debugger testing with this issue, and this is my conclusion, the devices ignore these directives completely.
Even in regular use, I've found that some mobiles also totally fail to check for new versions via say, Expires: 0 then checking last modified dates to determine if it should get a new one.
It simply doesn't happen, so what I was forced to do was add query strings to the css/js files I needed to force updates on, which tricks the stupid mobile devices into thinking it's a file it does not have, like: my.css?v=1, then v=2 for a css/js update. This largely works.
User browsers also, by the way, if left to their defaults, as of 2016, as I continuously discover (we do a LOT of changes and updates to our site) also fail to check for last modified dates on such files, but the query string method fixes that issue. This is something I've noticed with clients and office people who tend to use basic normal user defaults on their browsers, and have no awareness of caching issues with css/js etc, almost invariably fail to get the new css/js on change, which means the defaults for their browsers, mostly MSIE / Firefox, are not doing what they are told to do, they ignore changes and ignore last modified dates and do not validate, even with Expires: 0 set explicitly.
This was a good thread with a lot of good technical information, but it's also important to note how bad the support for this stuff is in particularly mobile devices. Every few months I have to add more layers of protection against their failure to follow the header commands they receive, or to properly interpet those commands.
This is answered directly in the MDN docs about cache control:
Most HTTP/1.0 caches don't support no-cache directives, so
historically max-age=0 was used as a workaround. But only
max-age=0 could cause a stale response to be reused when caches
disconnected from the origin server. must-revalidate addresses that.
That's why the example below is equivalent to no-cache.
Cache-Control: max-age=0, must-revalidate
But for now, you can simply use no-cache instead.
And also in the MDN docs about cache validation:
It is often stated that the combination of max-age=0 and
must-revalidate has the same meaning as no-cache.
Cache-Control: max-age=0, must-revalidate
max-age=0 means that the
response is immediately stale, and must-revalidate means that it must
not be reused without revalidation once it is stale — so in
combination, the semantics seem to be the same as no-cache.
However, that usage of max-age=0 is a remnant of the fact that many
implementations prior to HTTP/1.1 were unable to handle the no-cache
directive — and so to deal with that limitation, max-age=0 was used as
a workaround.
But now that HTTP/1.1-conformant servers are widely deployed, there's
no reason to ever use that max-age=0-and-must-revalidate combination —
you should instead just use no-cache.
One thing that (surprisingly) hasn't been mentioned is that a request can explicitly indicate that it will accept stale data, using the max-stale directive. In that case, if the server responded with max-age=0, the cache would merely consider the response stale, and would be free to use it to satisfy the client's request [which asked for potentially-stale data]. By contrast, if the server sends no-cache that really does trump any request by the client (with max-stale) for stale data, as the cache MUST revalidate.

Resources