Lesson 4-3 covered why safe GET responses are ideal for caching.
Serving a stored copy of a read is harmless, while replaying a stored answer to a write like POST would be wrong.
A cached copy of "read me the article" is just as good as a fresh one. A POST changes things on the server, so answering it from a stored copy would skip the change entirely.
This is why caching in practice is about GET, and this lesson is about the headers that control it.
The fastest request is no request
Your browser keeps a cache: a folder of responses it already received. If a page's logo can be reused from disk, the browser skips the entire lesson 1-2 journey for it: no DNS, no TCP, no TLS, no request. That is why the second visit to a site feels instant.
But reusing a stored copy is only correct if the server allows it. The server grants permission per response with the Cache-Control header:
Cache-Control: public, max-age=86400The common directives:
max-age=86400: this copy is fresh for 86400 seconds (24 hours). Reuse it freely until then.no-store: never store this (bank balances, personal data).no-cache: store it, but re-check with the server before each reuse.public/private: whether shared caches between you and the server (like the CDNs in the next lesson) may store it, or only your own browser.
There is also a clever middle ground. The server can tag a response with a version fingerprint header, ETag: "abc123". When the cached copy goes stale, the browser asks: If-None-Match: "abc123". If the content has not changed, the server answers 304 Not Modified with no body, and the browser keeps using its copy. You met 304 in the lesson 4-3 table, this is where it comes from.
Reading a max-age value
The same trim-until-the-interesting-part technique from lesson 1-2, applied twice, then converted to hours.
header="Cache-Control: public, max-age=86400" value=${header#Cache-Control: } maxage=${value#*max-age=} maxage=${maxage%%,*} hours=$((maxage / 3600)) echo "cache for $maxage seconds ($hours hours)"
Output
cache for 86400 seconds (24 hours)
The second trim is what handles the public, prefix. ${value#*max-age=} throws away everything up to the directive you want, no matter what else was listed before it.
The third line then cuts at the next comma, which matters because directives often follow max-age. Without it, a header ending in , immutable would leave that text glued to the number and break the arithmetic.
Dividing by 3600 is only for human comprehension. 86400 is unreadable at a glance while "24 hours" is immediately meaningful, and that translation is worth doing whenever you print a duration.
A tiny cache-policy reader
A case statement branches on which directive is present, in the same shape as the status-code classifier in lesson 4-3.
for header in "Cache-Control: max-age=3600" "Cache-Control: no-store" "Cache-Control: public, max-age=31536000, immutable"; do value=${header#Cache-Control: } case $value in *no-store*) echo "never cache" ;; *max-age=*) m=${value#*max-age=}; m=${m%%,*}; echo "cache for $m seconds" ;; *) echo "no caching info" ;; esac done
Output
cache for 3600 seconds never cache cache for 31536000 seconds
Reading the branches
- The
*no-store*branch comes first so it wins even when both directives appear, sincecasetakes the first matching pattern and stops. Order encodes the priority. - Inside the
max-agebranch, the two-step trim from the previous block reappears asm=${value#*max-age=}thenm=${m%%,*}, which is why the third header's trailingimmutabledoes no harm. - 31536000 seconds is one year, the classic value for files that never change. Combined with
immutableit tells the browser not even to re-check. - A response with neither directive falls to the catch-all. Real caches then apply their own heuristics, which is why leaving
Cache-Controloff is a decision rather than a neutral default.
What a 304 carries
Nothing. The empty body is the message, meaning your copy is still good.
304 is the whole point of ETags. When nothing changed, the server sends just the status line and headers and skips the entire body transfer, and the browser then serves its cached copy.
For a large asset that is a few hundred bytes of headers instead of megabytes of content, at the cost of one round trip to ask.
That cost is why max-age still matters even with ETags in place. A fresh cached copy needs no request at all, while a stale one needs a round trip to confirm, so the ideal setup uses a long max-age on files whose names change when their contents do.