HTTP - Caching

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

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

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

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-age is 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 it
  • private — only this user's browser may cache it, not a shared proxy
  • public — OK to store in a shared cache
  • You will mostly send max-age on 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

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-Length of the original file
  • The browser already has the bytes. You are only confirming they are current
  • You may send the ETag and Cache-Control again 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-age or Last-Modified. You still need ETags either way
  • max-age is the straightforward timeout. Prefer it

If-Modified-Since

  • Browser sends the Last-Modified value 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
  • Date on 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 /page with Accept-Language: en and Accept-Language: es are different cache entries
  • If you skip Vary, an English user can be served the Spanish page from cache
  • Common on Accept and Accept-Language

Do not cache forever

  • Dude I know set max-age to 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-age to expire
  • The old URL can stay cached. Nobody requests it anymore

What the server does

On GET /public/...:

  1. Compute the current ETag (and/or Last-Modified)
  2. If If-None-Match matches (or If-Modified-Since says it is not newer): send 304
  3. Otherwise send 200 with:
    • the file body
    • Cache-Control: max-age=10
    • ETag
    • Content-Type and Content-Length

Testing

  • Use a short max-age (10 seconds)
  • First request: 200, look for ETag and Cache-Control
  • Reload immediately: browser may not even send a request (still fresh)
  • Wait for max-age to expire, reload: If-None-Match and a 304
  • Change the file, wait, reload: 200 and a different ETag
  • DevTools → Network → "Disable cache" will hide all of this. Leave it unchecked

Further Reading