|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Why Do We Have a Cache-Control Request Header?" |
| 4 | +date: 2025-03-07 15:55:04 |
| 5 | +categories: Web Development |
| 6 | +meta: "Learn how the Cache-Control request header works, how browsers handle refresh and hard refresh caching, and when developers should use it for realtime data and offline-first applications." |
| 7 | +--- |
| 8 | + |
| 9 | +I’ve [written](/2019/03/cache-control-for-civilians/) and |
| 10 | +[spoken](https://slideslive.com/39021005/cache-rules-everything) many, many |
| 11 | +times about the `Cache-Control` response header and its many directives, but one |
| 12 | +thing I haven’t covered before—and something I don’t think many developers are |
| 13 | +even aware of—is the `Cache-Control` _request_ header. Unless you know your |
| 14 | +caching well, those two links in the first sentence will make this article a lot |
| 15 | +easier to understand. Maybe pop them open in another tab as a reference. |
| 16 | + |
| 17 | +Let’s go! |
| 18 | + |
| 19 | +## `Cache-Control` Recap |
| 20 | + |
| 21 | +As developers, we’re most used to `Cache-Control` as the preferred way of |
| 22 | +instructing caches (usually browsers) on how they should store responses (if at |
| 23 | +all), and what to do once their cache lifetime is up. Maybe something like this: |
| 24 | + |
| 25 | +``` |
| 26 | +Cache-Control: max-age=2147483648, immutable |
| 27 | +``` |
| 28 | + |
| 29 | +There’s a lot more to it than that, which you can read about in my 2019 piece, |
| 30 | +[<cite>Cache-Control for |
| 31 | +Civilians</cite>](/2019/03/cache-control-for-civilians/) |
| 32 | + |
| 33 | +One thing we’re probably less used to is `Cache-Control`’s employment as |
| 34 | +a _request_ header. |
| 35 | + |
| 36 | +## `Cache-Control` as a Request Header |
| 37 | + |
| 38 | +In a nutshell, the `Cache-Control` request header determines whether the browser |
| 39 | +retrieves content from the cache or forces a network request. It’s also used by |
| 40 | +intermediaries such as CDNs to work out whether they should serve a response |
| 41 | +themselves, or keep passing the request back upstream to origin. |
| 42 | + |
| 43 | +It’s a way for the client to force freshness. |
| 44 | + |
| 45 | +This means that the most common way you’re ever likely to see a `Cache-Control` |
| 46 | +request header is when you refresh or hard refresh a page. Honestly, that’s |
| 47 | +mostly it. |
| 48 | + |
| 49 | +All browsers behave a little differently between refreshes, hard refreshes, and |
| 50 | +run of the mill |
| 51 | +[revalidation](https://speakerdeck.com/csswizardry/cache-rules-everything?slide=10). |
| 52 | + |
| 53 | +## Refresh |
| 54 | + |
| 55 | +In **Chrome**, even if the page is still |
| 56 | +[fresh](https://speakerdeck.com/csswizardry/cache-rules-everything?slide=14), |
| 57 | +refreshing it will dispatch a request to the network with the following request |
| 58 | +headers: |
| 59 | + |
| 60 | +``` |
| 61 | +Cache-Control: max-age=0 |
| 62 | +[If-Modified-Since|If-None-Match] |
| 63 | +``` |
| 64 | + |
| 65 | +* `Cache-Control: max-age=0` just means after zero seconds, revalidate this |
| 66 | + resource. This isn’t incredibly strictly enforced so it’s technically a weak |
| 67 | + instruction to revalidate. More on that later. |
| 68 | +* The `If-Modified-Since` or `If-None-Match` headers are revalidation request |
| 69 | + headers that are used to compare the current version of the response with the |
| 70 | + target version on the network. |
| 71 | + |
| 72 | +All other subresources on the page are fetched as per their caching headers, so |
| 73 | +there is no different or specific behaviour here. |
| 74 | + |
| 75 | +In **Firefox** the behaviour is a little different. Refreshing a still-fresh |
| 76 | +page results in the following request headers: |
| 77 | + |
| 78 | +``` |
| 79 | +[If-Modified-Since|If-None-Match] |
| 80 | +``` |
| 81 | + |
| 82 | +No `Cache-Control` request header at all, just the relevant revalidation |
| 83 | +headers. |
| 84 | + |
| 85 | +Again, all of the page’s subresources are treated as normal. |
| 86 | + |
| 87 | +**Safari** is different still, and generally seems much more aggressive with its |
| 88 | +cache busting. Refreshing the same still-fresh page in Safari gives the |
| 89 | +following request headers: |
| 90 | + |
| 91 | +``` |
| 92 | +Cache-Control: no-cache |
| 93 | +Pragma: no-cache |
| 94 | +``` |
| 95 | + |
| 96 | +We have a `Cache-Control` request header, this time with a `no-cache` directive. |
| 97 | +While this is functionally equivalent to `max-age=0`, the spec speaks much more |
| 98 | +clearly that `no-cache` means that <q>a cache MUST NOT use the response to |
| 99 | +satisfy a subsequent request without successful revalidation with the origin |
| 100 | +server</q>. We also have the first appearance of `Pragma`, also carrying |
| 101 | +`no-cache`. `Pragma` is an incredibly outdated header that serves as a backward |
| 102 | +compatibility measure for HTTP/1.0 caches. Safari including this here is a very |
| 103 | +defensive measure! |
| 104 | + |
| 105 | +Again, all other subresources are treated as they would be normally. |
| 106 | + |
| 107 | +All browsers exhibit some similarities and some differences. |
| 108 | + |
| 109 | +* Even if the main document was still fresh in HTTP cache, a refresh will always |
| 110 | + put a request out onto the network in all browsers. |
| 111 | +* Chrome and Firefox emit revalidation headers which mean that, even though |
| 112 | + we’ve refreshed the page, we might still get served our locally cached version |
| 113 | + (`304`) if it’s still valid. |
| 114 | +* Firefox doesn’t emit a `Cache-Control` header, making it the least aggressive |
| 115 | + of the three. |
| 116 | +* Safari is by far the most aggressive, emitting both `Cache-Control` and |
| 117 | + `Pragma` headers, and no revalidation headers for potential reuse. Safari will |
| 118 | + always return a `200` response to a refresh. |
| 119 | + |
| 120 | +## Hard Refresh |
| 121 | + |
| 122 | +Things are a little different when it comes to a hard refresh. Hard refreshes |
| 123 | +are usually a sign of user frustration and that something is badly broken or |
| 124 | +outdated. To this end, browsers begin upping the ante here. |
| 125 | + |
| 126 | +In **all browsers**, a hard refresh causes both the main document and all of its |
| 127 | +subresources to be requested with the following: |
| 128 | + |
| 129 | +``` |
| 130 | +Cache-Control: no-cache |
| 131 | +Pragma: no-cache |
| 132 | +``` |
| 133 | + |
| 134 | +Key things to note: |
| 135 | + |
| 136 | +* No browser emits a revalidation header, meaning a `304` is not possible. We’re |
| 137 | + always guaranteed a fresh response. |
| 138 | +* Chrome switched from `max-age=0` to `no-cache`. This is a clear signal of |
| 139 | + intent that a hard refresh is more aggressive than a regular one. |
| 140 | +* Safari’s behaviour remains unchanged, which means that as far as the main |
| 141 | + document is concerned, a refresh and a hard refresh are equivalent. |
| 142 | + |
| 143 | +Note that this all applies to the main document and all of its subresources—even |
| 144 | +[`immutable`](/2019/03/cache-control-for-civilians/#immutable) assets—so |
| 145 | +everything on the page is now guaranteed fresh. `304` responses are not |
| 146 | +possible. |
| 147 | + |
| 148 | +### `max-age=0` vs `no-cache` |
| 149 | + |
| 150 | +Both of these directives behave incredibly similarly: `max-age=0` means the |
| 151 | +response is considered stale after zero seconds and therefore should be |
| 152 | +revalidated, and `no-cache` means don’t fetch this response from cache without |
| 153 | +revalidating it first. |
| 154 | + |
| 155 | +Where they differ is that `max-age=0` permits caches to reuse a response if |
| 156 | +revalidation isn't possible (e.g. no network access); `no-cache` is much |
| 157 | +stricter—it means the cache must always revalidate before releasing a response, |
| 158 | +or return an error if revalidation fails. |
| 159 | + |
| 160 | +## Revalidation |
| 161 | + |
| 162 | +In the case that a user hasn’t refreshed the page, but instead they have a file |
| 163 | +in their cache that is now considered |
| 164 | +[stale](https://speakerdeck.com/csswizardry/cache-rules-everything?slide=15), |
| 165 | +the browser needs to check with the server whether or not it needs a new copy, |
| 166 | +or if it can reuse and renew the previously cached version. This is called |
| 167 | +_revalidation_ and is when the `If-Modified-Since` or `If-None-Match` headers |
| 168 | +come into play. |
| 169 | + |
| 170 | +* **`If-Modified-Since`** is used to check a file against its `Last-Modified` |
| 171 | + response header. |
| 172 | +* **`If-None-Match`** is used to check a file against its `Etag` response |
| 173 | + header. |
| 174 | + |
| 175 | +When a file needs revalidating, **all browsers** behave the same: |
| 176 | + |
| 177 | +``` |
| 178 | +[If-Modified-Since|If-None-Match] |
| 179 | +``` |
| 180 | + |
| 181 | +They attach the relevant revalidation header, which will result in either |
| 182 | +a `200` or `304` response in most cases. This is unremarkable other than the |
| 183 | +fact that no browser attaches a `Cache-Control` request header at this point. |
| 184 | + |
| 185 | +## When to Use a `Cache-Control` Request Header |
| 186 | + |
| 187 | +Each of these use cases was browser-defined, very much out of our hands as web |
| 188 | +developers, but there are scenarios when we might want to (and can!) add our own |
| 189 | +`Cache-Control` request headers. Think of these scenarios as incredibly |
| 190 | +aggressive, incredibly defensive bidirectional caching rules to absolutely |
| 191 | +guarantee that no caches anywhere along with request–response lifecycle will |
| 192 | +retain a copy of a response. By setting `Cache-Control` at both ends, we have |
| 193 | +a double-pronged approach to our strategy. A very cautious approach. |
| 194 | + |
| 195 | +### Realtime Data |
| 196 | + |
| 197 | +Imagine you’re building a sports betting site or stock trading app: realtime |
| 198 | +price updates are incredibly important, and all data must be up to date, |
| 199 | +_always_. You’d serve your _responses_ with something like: |
| 200 | + |
| 201 | +``` |
| 202 | +Cache-Control: no-store |
| 203 | +``` |
| 204 | + |
| 205 | +…and make your _requests_ with something like: |
| 206 | + |
| 207 | +```js |
| 208 | +fetch("https://api.website.com/data", { |
| 209 | + method: "GET", |
| 210 | + headers: { |
| 211 | + "Cache-Control": "no-store", |
| 212 | + } |
| 213 | +}) |
| 214 | +``` |
| 215 | + |
| 216 | +This is the bare minimum for modern and compliant caches, but the |
| 217 | +hyper-defensive version would be more like: |
| 218 | + |
| 219 | +``` |
| 220 | +Cache-Control: no-store, no-cache, max-age=0, must-revalidate |
| 221 | +Pragma: no-cache |
| 222 | +``` |
| 223 | + |
| 224 | +…on your responses, and this in your requests: |
| 225 | + |
| 226 | +```js |
| 227 | +fetch("https://api.website.com/data", { |
| 228 | + method: "GET", |
| 229 | + headers: { |
| 230 | + "Cache-Control": "no-store, no-cache, max-age=0", |
| 231 | + "Pragma": "no-cache" |
| 232 | + } |
| 233 | +}) |
| 234 | +``` |
| 235 | + |
| 236 | +The latter two examples are overkill and do contain a lot of redundancy, but |
| 237 | +they also won’t do any harm. |
| 238 | + |
| 239 | +<small>Note that if the data is also potentially sensitive and contains |
| 240 | +user-specific data, you’d want to [add `private` to your `Cache-Control` |
| 241 | +response |
| 242 | +headers](https://www.linkedin.com/feed/update/urn:li:activity:7303763824388558848/).</small> |
| 243 | + |
| 244 | +### Offline Applications |
| 245 | + |
| 246 | +If you have an offline application, you can use `only-if-cached` to _only_ serve |
| 247 | +a response if it’s in cache, otherwise returning a `504`. |
| 248 | + |
| 249 | +```js |
| 250 | +fetch("https://api.website.com/offline-data", { |
| 251 | + method: "GET", |
| 252 | + headers: { |
| 253 | + "Cache-Control": "only-if-cached" |
| 254 | + } |
| 255 | +}) |
| 256 | +``` |
| 257 | + |
| 258 | +Adding this request header ensures that the request would never hit the network. |
| 259 | + |
| 260 | +While `only-if-cached` might not be useful for most web pages, it can be handy |
| 261 | +for offline-first applications, such as PWAs or news readers that prefer using |
| 262 | +stored content rather than attempting a network request that might fail. |
| 263 | + |
| 264 | +## Final Takeaways |
| 265 | + |
| 266 | +* Browsers automatically send a mix `Cache-Control`, `Pragma`, or revalidation |
| 267 | + headers in refresh and hard refresh scenarios. |
| 268 | +* `max-age=0` and `no-cache` both trigger revalidation, but `no-cache` is much |
| 269 | + stricter and requires a fresh response. |
| 270 | +* You can manually use `Cache-Control` in requests when you need realtime data |
| 271 | + or offline-first apps. |
| 272 | +* To force freshness, use: |
| 273 | + ```http |
| 274 | + Cache-Control: no-store, no-cache, max-age=0, must-revalidate |
| 275 | + ``` |
| 276 | +* To build offline-first apps, consider: |
| 277 | + ```http |
| 278 | + Cache-Control: only-if-cached |
| 279 | + ``` |
0 commit comments