How Different Browsers Handle Cache-Control Settings Differently

23-NOV-2018, last update 08-DEC-2018
HTTP response headers can be used to instruct a web browser how to cache or not cache the data. To improve your understanding how this works, there are many good articles, such as Google's HTTP-caching article.
I like the Mozilla Developer Network documentation about HTTP Headers. The formal specification can be found in RFC 7234.
HTML pages can contain META tags that provide information equivalent to headers.

This article describes how these instructions are handled in the different brands of web browsers in different situations, and it focuses on the differences. The differences are substantial! The article might explain some odd behavior you have seen from time to time, or explain incident reports from your users.

Table of Contents


This article only covers directives that are relevant to the web browser.
Because of that, it does not cover directives that refer to intermediate proxies:

Introduction - The Concepts

When it comes to caching of web page and linked resouces, the following concepts are relevant: ↑ Back to menu

No headers

By default (in the absence of cache related headers), a web browser will assume the requested web page or resource is: An example, which can be found here.
GET /hello/default.asp HTTP/1.1

HTTP/1.1 200 OK
Date: Fri, 23 Nov 2018 14:32:33 GMT
Content-Length: 373
The browser may choose to store this result in the cache. When you navigate forward or backward to this page, or reload the page, the web browser may choose to reuse the cached version immediately.

“Microsoft Edge combines cached with uncached data!”

The table below shows the behavior when all caching related headers and meta tags are absent.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Safari 12
Internet Explorer 11
Edge 42
Backward on
Chrome 70
FireFox 63
Safari 12
Internet Explorer 11
Backward on
Edge 42
Reload on
Chrome 70
FireFox 63
Safari 12
Internet Explorer 11
Edge 42
page<none>no matcherscachecacheno matchers
linked resource<none>no matcherscachecacheno matchers
3rd party iframed page<none>no matcherscacheNO MATCHERSno matchers
3rd party iframed linked resource<none>no matcherscacheNO MATCHERSno matchers

Note that Edge reloads the iframed resources, while using cached versions of the main resources. This can result in an inconsistent user experience.

↑ Back to menu

If-None-Match - Etag

The server can specify the version of a resource with an Entity Tag. The server communicates this with the "Etag" response header.

If the web browser has a cached version with an Entity Tag and wants to verify if it is still current (= "fresh"), it will include the "If-None-Match" request header with one or more Entity Tags. The server evaluates this. If any of the Entity Tags match, the server will respond with a 304 Not Modified status and include the current Entity Tag as well.

An example, which can be found here.
GET /hello/etag.asp HTTP/1.1

HTTP/1.1 200 OK
ETag: W/"0029",
Date: Fri, 23 Nov 2018 17:11:25 GMT
Content-Length: 394
GET /hello/etag.asp HTTP/1.1
If-None-Match: W/"0029"

HTTP/1.1 304 Not Modified
ETag: W/"0029",
Date: Fri, 23 Nov 2018 17:32:21 GMT
GET /hello/etag.asp HTTP/1.1
If-None-Match: W/"0029"

HTTP/1.1 200 OK
ETag: W/"3059",
Date: Fri, 23 Nov 2018 17:35:32 GMT
Content-Length: 432
“Safari doesn't seem to know what it is doing...”

The table below shows the behavior when Etags are used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
Single INM = a network request was done with a If-None-Match header with a single Etag, the server responded with HTTP status code 304 Not Modified, the web browser used the cached version
Single INM and no matchers = Single INM is performed, followed by no matchers cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Safari 12
Edge 42
Forward on
Internet Explorer 11
Backward on
Chrome 70
FireFox 63
Safari 12
Internet Explorer 11
Backward on
Edge 42
Reload on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Reload on
Safari 12
pageEtagSingle INMSingle INMcachecacheSingle INMno matchers
linked resourceEtagSingle INMCACHEcachecacheSingle INMSINGLE INM and NO MATCHERS
3rd party iframed pageEtagSingle INMSingle INMcacheSINGLE INMSingle INMSingle INM
3rd party iframed linked resourceEtagSingle INMSingle INMcacheSINGLE INMSingle INMSingle INM

So none of the web browsers support multiple cached version of the same resource.

Note that Safari doesn't seem to know what it is doing when refreshing, resulting in excess calls for linked resources.

Also note that IE11 does not revalidate linked resources. This can create inconsistencies with an outdated linked resource in an updated web page.
And as mentioned before, Edge reloads the iframed resources when backing up.

↑ Back to menu

If-Modified-Since - Last-Modified

The server can specify the version of a resource with its date. The server communicates this with the "Last-Modified" response header.

If the web browser has a cached version with a Last Modified date and time and wants to verify if it is still current, it will include the "If-Modified-Since" request header with date and time of the cached version. The server evaluates this and if the version is still current, it responds with a 304 Not Modified status.

Example 1, which can be found here.
GET /hello/last-modified.asp HTTP/1.1

HTTP/1.1 200 OK
Last-Modified: Sat, 17 Nov 2018 23:33:51 GMT
Date: Fri, 23 Nov 2018 22:32:25 GMT
GET /hello/last-modified.asp HTTP/1.1
If-Modified-Since: Sat, 17 Nov 2018 23:33:51 GMT

HTTP/1.1 304 Not Modified
Date: Fri, 23 Nov 2018 22:40:28 GMT
“In Chrome you cannot fix caching issues with a normal reload.”

The table below shows the behavior when Last-Modified header is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
IMS = a network request was done with the If-Modified-Since, the server responded with HTTP status code 304 Not Modified, the web browser used the cached version
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Reload on
FireFox 63
Internet Explorer 11
Edge 42
Reload on
Chrome 70
Reload on
Safari 12
pageLast-ModifiedcachecacheIMSIMSno matchers
linked resourceLast-ModifiedcachecacheIMSCACHEfirst CACHE, after that IMS
3rd party iframed pageLast-ModifiedcachecacheIMSCACHECACHE
3rd party iframed linked resourceLast-ModifiedcachecacheIMSCACHECACHE

Under normal working conditions, resources with a Last-Modified-Date always cached.

Note that in Chrome you cannot fix caching issues with a normal reload.
A hard reload does fix the problem, because it will request all resources without any "If-*" headers.

Combination Etag & Last-Modified

Etags and Last-Modified can be combined.

If the web browser has a cached version with a Last Modified date and an Etag, it will include both a "If-Modified-Since" and a "If-None-Match" request headers. The server evaluates this and if the version is still current, it responds with a 304 Not Modified status.

Example 2, which can be found here.
GET /hello/last-modified-etag.asp HTTP/1.1

HTTP/1.1 200 OK
Last-Modified: Sat, 17 Nov 2018 23:39:59 GMT
ETag: W/"3059",
GET /hello/last-modified-etag.asp HTTP/1.1
If-None-Match: W/"3059"
If-Modified-Since: Sat, 17 Nov 2018 23:39:59 GMT

HTTP/1.1 304 Not Modified
ETag: W/"3059",
Date: Fri, 23 Nov 2018 23:16:47 GMT
“Why does Google Chrome not send If-None-Match header?”

The table below shows the behavior when both a Last-Modified and Etag header are used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
IMS/INM = a network request was done with the If-Modified-Since and If-None-Match headers, the server responded with HTTP status code 304 Not Modified, the web browser used the cached version
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Reload on
FireFox 63
Internet Explorer 11
Edge 42
Reload on
Chrome 70
Reload on
Safari 12
pageLast-Modified & EtagcachecacheIMS/INMIMS/INMno matchers
linked resourceLast-Modified & EtagcachecacheIMS/INMCACHEfirst CACHE, after that IMS
3rd party iframed pageLast-Modified & EtagcachecacheIMS/INMCACHECACHE
3rd party iframed linked resourceLast-Modified & EtagcachecacheIMS/INMCACHECACHE

So why does Google Chrome not send If-None-Match header?
Because just like any major web browsers, it will simply use the cached version of resources that returned a Last-Modified-Date.
...That is, unless the appropriate Cache Control directives or Expires is used.

↑ Back to menu

Cache-Control: must-revalidate

The server can specify that the client should revalidate a resource. The server communicates this with the "must-revalidate" value of the Cache-Control response header.

Example 1, which can be found here.
GET /hello/must-revalidate.asp HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: must-revalidate
Date: Sat, 24 Nov 2018 17:21:59 GMT
“Internet Explorer does not revalidate linked resources”

The table below shows the behavior when the must-revalidate directive is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Edge 42
Safari 12
Forward on
Internet Explorer 11
Backward on
Chrome 70
FireFox 63
Internet Explorer 11
Safari 12
Backward on
Edge 42
Reload on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
pagemust-revalidateno matchersno matcherscachecacheno matchers
linked resourcemust-revalidateno matchersCACHEcachecacheno matchers
3rd party iframed pagemust-revalidateno matchersno matcherscacheNO MATCHERSno matchers
3rd party iframed linked resourcemust-revalidateno matchersno matcherscacheNO MATCHERSno matchers

You might have expected a revalidation when clicking the browser Back button. But as you can see, must-revalidate is ignored when nagivating back.

Except in Edge, where the primary page and resources are not revalidated, but the iframed data is.

Also, Internet Explorer 11 is misbehaving; it does not revalidate linked resources.

Cache-Control: must-revalidate & Last-Modified

Example 2, which can be found here.
GET /hello/must-revalidate-last-modified.asp HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: must-revalidate
Last-Modified: Sat, 17 Nov 2018 23:33:51 GMT
Date: Sat, 24 Nov 2018 17:29:31 GMT
GET /hello/must-revalidate-last-modified.asp HTTP/1.1
If-Modified-Since: Sat, 17 Nov 2018 23:33:51 GMT

HTTP/1.1 304 Not Modified
Cache-Control: must-revalidate
Date: Sat, 24 Nov 2018 17:30:29 GMT
“must-revalidate does NOT fix the Last-Modified behavior”

The table below shows the behavior when both must-revalidate directive and Last-Modified header is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
IMS = a network request was done with the If-Modified-Since, the server responded with HTTP status code 304 Not Modified, the web browser used the cached version
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
Forward on
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Reload on
Chrome 70
FireFox 63
Internet Explorer 11
Reload on
Edge 42
Reload on
Safari 12
pagemust-revalidate & Last-Modified (not expired)IMSCACHEcacheIMSIMSno matchers
linked resourcemust-revalidate & Last-Modified (not expired)IMSCACHEcacheIMSIMSfirst CACHE, after that NO MATCHERS
3rd party iframed pagemust-revalidate & Last-Modified (not expired)IMSCACHEcacheIMSCACHECACHE
3rd party iframed linked resourcemust-revalidate & Last-Modified (not expired)IMSCACHEcacheIMSCACHECACHE

Although it has its effect in Google Chrome, must-revalidate does not fix the Last-Modified behavior most major browsers.

Also, Microsoft Edge behaves unexpected when it comes to refreshing the page.

↑ Back to menu

Expires

The server can specify an exact date and time after which the resource expires. The server communicates this with the "Expires" response header.

If the web browser has a cached version with Expiration information, then it will verify if it is in the future or the past. If it is in the future, the cached version will be used. Otherwise a fresh copy is requested.

Note that the (server) Expiry date and time is compared against the (client) local time. These clocks may not be properly synchronized, which may lead to unintended behavior, such as immediate expiration.

An example, which can be found here.
GET /hello/expires.asp HTTP/1.1

HTTP/1.1 200 OK
Expires: Sat, 24 Nov 2018 13:32:34 GMT
Date: Sat, 24 Nov 2018 13:31:33 GMT
“Expiry time is compared against local time”

The table below shows the behavior when Expires is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Chrome 70
FireFox 63
Internet Explorer 11
Safari 12
Backward on
Edge 42
Reload on
FireFox 63
Internet Explorer 11
Reload on
Chrome 70
Reload on
Edge 42
Reload on
Safari 12
pageExpires (not expired)cachecachecacheno matchersno matchersno matchersno matchers
linked resourceExpires (not expired)cachecachecacheno matchersCACHEno matchersfirst CACHE, after that NO MATCHERS
3rd party iframed pageExpires (not expired)cachecachecacheno matchersCACHECACHECACHE
3rd party iframed linked resourceExpires (not expired)cachecachecacheno matchersCACHECACHECACHE
pageExpires (expired)no matcherscachecacheno matchersno matchersno matchersno matchers
linked resourceExpires (expired)no matcherscachecacheno matchersno matchersno matchersno matchers
3rd party iframed pageExpires (expired)no matcherscacheNO MATCHERSno matchersno matchersno matchersno matchers
3rd party iframed linked resourceExpires (expired)no matcherscacheNO MATCHERSno matchersno matchersno matchersno matchers

The Chrome reload issue was mentioned before. Apart from that, no unexpected behavior.

↑ Back to menu

Cache-Control: max-age

The server can specify a duration in seconds after which resource expires. The server communicates this with the "max-age" value in the "Cache-Control" response header.

If the web browser has a cached version with max-age information, then it will verify if that time has past and use the cached version or request a fresh copy.

An example, which can be found here.
GET /hello/max-age.asp HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: max-age=30
Date: Sat, 24 Nov 2018 14:40:37 GMT
The table below shows the behavior when max-age is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Chrome 70
FireFox 63
Internet Explorer 11
Safari 12
Backward on
Edge 42
Reload on
FireFox 63
Internet Explorer 11
Reload on
Chrome 70
Reload on
Edge 42
Reload on
Safari 12
pagemax-age (not expired)cachecachecacheno matchersno matchersno matchersno matchers
linked resourcemax-age (not expired)cachecachecacheno matchersCACHEno matchersfirst CACHE, after that NO MATCHERS
3rd party iframed pagemax-age (not expired)cachecachecacheno matchersCACHECACHECACHE
3rd party iframed linked resourcemax-age (not expired)cachecachecacheno matchersCACHECACHECACHE
pagemax-age (expired)no matcherscachecacheno matchersno matchersno matchersno matchers
linked resourcemax-age (expired)no matcherscachecacheno matchersno matchersno matchersno matchers
3rd party iframed pagemax-age (expired)no matcherscacheNO MATCHERSno matchersno matchersno matchersno matchers
3rd party iframed linked resourcemax-age (expired)no matcherscacheNO MATCHERSno matchersno matchersno matchersno matchers

The Chrome reload issue was mentioned before. Apart from that, no unexpected behavior.

Expires & Cache-Control: max-age

When both Expires and max-age are used, then max-age is supposed to take precedence.

Two examples, which can be found here and here.
GET /hello/expires-max-age.asp HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: max-age=30
Expires: Sat, 24 Nov 2018 15:10:05 GMT
Date: Sat, 24 Nov 2018 15:09:04 GMT
“max-age takes precedence over Expires”

The table below shows the behavior when both Expires and max-age is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Chrome 70
FireFox 63
Internet Explorer 11
Safari 12
Backward on
Edge 42
Reload on
FireFox 63
Internet Explorer 11
Edge 42
Reload on
Chrome 70
Reload on
Edge 42
Reload on
Safari 12
pagemax-age (expired), Expires (not expired)no matcherscachecacheno matchersno matchersno matchersno matchers
linked resourcemax-age (expired), Expires (not expired)no matcherscachecacheno matchersno matchersno matchersno matchers
3rd party iframed pagemax-age (expired), Expires (not expired)no matcherscacheNO MATCHERSno matchersno matchersno matchersno matchers
3rd party iframed linked resourcemax-age (expired), Expires (not expired)no matcherscacheNO MATCHERSno matchersno matchersno matchersno matchers
pagemax-age (not expired), Expires (expired)cachecachecacheno matchersno matchersno matchersno matchers
linked resourcemax-age (not expired), Expires (expired)cachecachecacheno matchersCACHEno matchersfirst CACHE, after that NO MATCHERS
3rd party iframed pagemax-age (not expired), Expires (expired)cachecachecacheno matchersCACHECACHECACHE
3rd party iframed linked resourcemax-age (not expired), Expires (expired)cachecachecacheno matchersCACHECACHECACHE

As you can see, practice matches theory; max-age takes precedence over Expires.

The Chrome reload issue was mentioned before. Apart from that, no unexpected behavior.

↑ Back to menu

Cache-Control: no-cache

The server can specify that if the response is stored, it should be revalidated before begin reused. The server communicates this with the "no-cache" value in the "Cache-Control" response header.

An example, which can be found here.
GET /hello/no-cache.asp HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: no-cache
Date: Sat, 08 Dec 2018 17:48:03 GMT
“You might expect no-cache to prevent all caching...”

The table below shows the behavior when no-cache is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Chrome 70
FireFox 63
Internet Explorer 11
Safari 12
Backward on
Edge 42
Reload on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
pageno-cacheno matcherscachecacheno matchers
linked resourceno-cacheno matcherscachecacheno matchers
3rd party iframed pageno-cacheno matcherscacheno matchersno matchers
3rd party iframed linked resourceno-cacheno matcherscacheno matchersno matchers

You might expect "no-cache" to prevent all caching, but as you can see it doesn't.

Cache-Control: no-store

The server can specify that the response is not allowed to be stored. Since the client shouldn't store it, it can never validate a cached copy. The server communicates this with the "no-store" value in the "Cache-Control" response header.

An example, which can be found here.
GET /hello/no-store.asp HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: no-store
Date: Sat, 08 Dec 2018 17:30:00 GMT
“Internet Explorer 11 ignores no-store”

The table below shows the behavior when no-store is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Chrome 70
FireFox 63
Edge 42
Backward on
Internet Explorer 11
Safari 12
Reload on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
pageno-storeno matchersno matchersCACHEno matchers
linked resourceno-storeno matchersno matchersCACHEno matchers
3rd party iframed pageno-storeno matchersno matchersCACHEno matchers
3rd party iframed linked resourceno-storeno matchersno matchersCACHEno matchers

As you can see, Internet Explorer and Safari ignore no-store when navigating back.

Cache-Control: no-cache, no-store, must-revalidate

To prevent all caching, many sources recommend the combination of no-cache, no-store and must-revalidate. I guess browsers are much alike some children, which you have to tell not to do something over and over again, in different working.

An example, which can be found here.
GET /hello/no-cache-no-store-must-revalidate.asp HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, must-revalidate
Date: Sat, 08 Dec 2018 17:58:43 GMT
“It feels as if bugs are put in deliberately”

The table below shows the behavior when the combination of no-cache, no-store and must-revalidate is used.

no matchers = a network request was done without cache specific headers, the server responded with HTTP status code 200 OK and returned the page / resource, the web browser used the returned data
cache = no network call was done, the web browser used the cached version

ResourceSituation Forward on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
Backward on
Edge 42
Backward on
Chrome 70
FireFox 63
Backward on
Internet Explorer 11
Safari 12
Reload on
Chrome 70
FireFox 63
Internet Explorer 11
Edge 42
Safari 12
pageno-cache, no-store, must-revalidateno matchersno matchersno matchersCACHEno matchers
linked resourceno-cache, no-store, must-revalidateno matchersno matchersno matchersCACHEno matchers
3rd party iframed pageno-cache, no-store, must-revalidateno matchersno matchersCACHECACHEno matchers
3rd party iframed linked resourceno-cache, no-store, must-revalidateno matchersno matchersno matchersCACHEno matchers

The unexpected thing here, is that with just "no-store", Firefox and Chrome will not cache iframed pages. But when using it in combination with "no-cache" and "must-revalidate", it stops revalidating. It almost feels as if bugs are put in deliberately.

Also note, that this combination of directives does not force Internet Explorer or Safari to reload the content when navigating back.
↑ Back to menu

As a side note, the cookie behavior of web browsers is also very inconsistent.

Mail your comments to gertjans@xs4all.nl.