Skip to content

Commit 325debf

Browse files
committed
Add new Cache Control article
1 parent 4b0cb26 commit 325debf

File tree

1 file changed

+279
-0
lines changed

1 file changed

+279
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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

Comments
 (0)