sequenceDiagram participant Browser participant Server Browser->>Server: GET /style.css Server->>Browser: 200 + max-age=10 Note over Browser: Cache the response Note over Browser: A few seconds later: reuse the cache Note over Server: No second request
HTTP - Caching
How browsers cache HTTP responses using timeouts, ETags, and 304 Not Modified
Caching
Why cache
- Loading a page is still a lot of requests, even with keep-alive
- CSS, JS, images, and fonts rarely change
- Sending the same file over the network every time is wasted work
- Caching: reuse a previous response instead of downloading it again
Who stores the cache?
- The browser stores the response
- Your server does not keep a cache of responses
- Your job is to send headers that tell the browser:
- How long this response can be reused
- How to check whether it is still valid
- Same URL later: the browser decides whether to reuse, revalidate, or request from scratch
Fresh vs stale
- Fresh: the cached copy is still good. Use it. Do not talk to the server
- Stale: the cached copy might be outdated. Ask the server before using it
- Two independent mechanisms:
- Timeouts (
max-age,Expires): how long the copy stays fresh - Validators (
ETag,Last-Modified): how the server confirms it has not changed
- Timeouts (
Timeouts
Cache-Control: max-age
- Number of seconds the browser may reuse this response
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 42
Cache-Control: max-age=3600
max-age=3600→ fresh for one hour- After that, the copy is stale
- For homework, use a short timeout (about 10 seconds) so you can test it
Fresh: no request at all
The browser downloads style.css once and stores it. A few seconds later it needs the file again. The cache is still fresh, so the browser uses the stored copy and the server is not contacted.
Expires
- An exact time when the response becomes stale
Expires: Wed, 23 Sep 2026 19:50:00 GMT
- HTTP dates are GMT, not local time, not EST
max-ageis preferred- If both are present, the browser uses
max-age
Other Cache-Control values
no-store— do not save this at all (passwords, private pages)no-cache— you may store it, but revalidate before using itprivate— only this user's browser may cache it, not a shared proxypublic— OK to store in a shared cache- You will mostly send
max-ageon static files
Validators
When the cache is stale
- Timeout expired. Browser still has the old bytes
- It does not throw them away immediately
- It asks the server: "Is this still the current version?"
- If yes: 304 Not Modified — keep using the cached body
- If no: 200 OK with the new body
- That question is sent with a validator
ETag
- A fingerprint of this version of the resource
- Send it on the original 200 response
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 42
Cache-Control: max-age=10
ETag: "9e107d9d372bb6826bd81d3542a419d6"
- Quotes are part of the value
- When the file contents change, the ETag must change
- A hash of the file bytes is a solid way to do this
If-None-Match
- Once the copy is stale, the browser sends the ETag back
GET /public/style.css HTTP/1.1
Host: localhost:8080
If-None-Match: "9e107d9d372bb6826bd81d3542a419d6"
- Compare it to the ETag of the file on disk right now
- Same → nothing changed → 304
- Different → file changed → 200 with the new body and the new ETag
304 Not Modified
sequenceDiagram participant Browser participant Server Note over Browser: Cached copy is stale Browser->>Server: GET /public/style.css Note over Browser,Server: If-None-Match: "abc" Server->>Browser: 304 Not Modified Note over Browser: Reuse the cached body
The browser still has a copy of style.css, but it is stale. It sends If-None-Match with the stored ETag. The server compares it to the current file and replies 304 Not Modified. The browser keeps using the cached body.
304 has no body
HTTP/1.1 304 Not Modified
ETag: "9e107d9d372bb6826bd81d3542a419d6"
Cache-Control: max-age=10
- 300-level is not only redirects. 304 means "your copy is still good"
- Do not send the file
- Do not send a fake body with
Content-Lengthof the original file - The browser already has the bytes. You are only confirming they are current
- You may send the ETag and
Cache-Controlagain so the copy becomes fresh
Last-Modified
- The other validator: when this file last changed on disk
HTTP/1.1 200 OK
Content-Type: text/css
Content-Length: 42
Cache-Control: max-age=10
Last-Modified: Wed, 16 Sep 2026 18:00:00 GMT
- Homework: timeouts can be
max-ageorLast-Modified. You still need ETags either way max-ageis the straightforward timeout. Prefer it
If-Modified-Since
- Browser sends the
Last-Modifiedvalue back when revalidating
GET /public/style.css HTTP/1.1
Host: localhost:8080
If-Modified-Since: Wed, 16 Sep 2026 18:00:00 GMT
- Compare to the file's current modification time
- Not newer → 304
- Newer → 200 with the new body and a new
Last-Modified
Date formats will hurt you
- File timestamps on disk are not HTTP dates
- HTTP dates look like:
Wed, 16 Sep 2026 18:00:00 GMT - They are always GMT. Your laptop is probably not
Dateon the response is "now" in that same format- Parse and format carefully. Off-by-one-hour bugs look like "caching is broken"
- This is a good reason to prefer ETags plus
max-age
What to cache
Static files, not everything
- Cache responses for paths under
/public- CSS, JS, images, fonts
- Do not blindly cache:
- HTML that changes when users post
- JSON APIs with per-user data
- Anything that depends on a cookie / session
- If two users must not see each other's response, do not let a shared cache store it
Cache-Control: private
Cache-Control: private, max-age=60
- This user's browser may cache it
- A CDN or proxy in the middle must not
- Use this for anything tied to a logged-in session
- Static public files can be cached without
private
Vary
- By default the cache key is the URL
- Sometimes the same URL serves different content depending on a request header
Vary: Accept-Language
GET /pagewithAccept-Language: enandAccept-Language: esare different cache entries- If you skip
Vary, an English user can be served the Spanish page from cache - Common on
AcceptandAccept-Language
Do not cache forever
- Dude I know set
max-ageto several decades - Made a change
- Had to email a few clients who could not get the change
- "Can you please click these buttons to invalidate your cache"
- You cannot reach into someone else's browser and delete the file
- Short
max-age+ validators = fast and still correct
Cache busting
- Another way to force a new download: change the URL
/public/style.css?v=3
/public/style.3.css
- New URL → cache miss → browser fetches the new file
- Useful when you ship a new frontend and cannot wait for
max-ageto expire - The old URL can stay cached. Nobody requests it anymore
What the server does
On GET /public/...:
- Compute the current ETag (and/or
Last-Modified) - If
If-None-Matchmatches (orIf-Modified-Sincesays it is not newer): send 304 - Otherwise send 200 with:
- the file body
Cache-Control: max-age=10ETagContent-TypeandContent-Length
Testing
- Use a short
max-age(10 seconds) - First request: 200, look for
ETagandCache-Control - Reload immediately: browser may not even send a request (still fresh)
- Wait for
max-ageto expire, reload:If-None-Matchand a 304 - Change the file, wait, reload: 200 and a different ETag
- DevTools → Network → "Disable cache" will hide all of this. Leave it unchecked