Caching Strategies Lesson 4 of 6 ⏱️ 4 min read

HTTP Caching, ETags & CDNs

Cache-Control, ETag and Last-Modified headers, conditional requests, and how CDNs cache content at the edge.

Updated Oct 30, 2026

HTTP has caching built in

The fastest request is the one that never reaches your server. HTTP defines headers that tell browsers, proxies and CDNs whether a response can be cached, for how long, and how to check if it's still valid. Getting these headers right is free performance.

Cache-Control: the main switch

Directive Meaning
max-age=3600 Fresh for 3600 seconds in any cache
s-maxage=86400 Overrides max-age for shared caches (CDNs, proxies)
public Any cache may store it, even for authenticated requests
private Only the user's browser may store it (personal data)
no-cache Store it, but revalidate with the server before every use
no-store Never store it anywhere (sensitive data)
immutable Will never change during its lifetime — don't revalidate
stale-while-revalidate=60 Serve stale for up to 60 s while fetching a fresh copy in the background

Typical policies:

Plaintext
# Versioned static assets: app.3f9a1c.js
Cache-Control: public, max-age=31536000, immutable

# Public HTML page that changes occasionally
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60

# A logged-in user's dashboard
Cache-Control: private, no-cache

# Banking or payment responses
Cache-Control: no-store

no-cache does not mean "don't cache" — it means "check with me first". Use no-store when data must never be stored.

Validation: ETag and Last-Modified

When a cached response is stale, the client can ask "has it changed?" instead of downloading it again.

  • The server sends an ETag (a fingerprint of the content) or Last-Modified.
  • The client later sends If-None-Match: <etag> or If-Modified-Since: <date>.
  • If nothing changed, the server replies 304 Not Modified with no body.
Python
import hashlib
import json

def make_etag(body: bytes) -> str:
    return '"' + hashlib.sha256(body).hexdigest()[:16] + '"'

def handle_get(course: dict, if_none_match: str | None):
    body = json.dumps(course, sort_keys=True).encode()
    etag = make_etag(body)
    if if_none_match == etag:
        return 304, {"ETag": etag}, b""                      # client's copy is still valid
    return 200, {"ETag": etag, "Cache-Control": "no-cache"}, body

course = {"id": 1, "title": "Operating Systems"}
status, headers, body = handle_get(course, None)
print(status, headers["ETag"], len(body), "bytes")

status, headers, body = handle_get(course, headers["ETag"])   # browser revalidates
print(status, len(body), "bytes")

course["title"] = "Operating Systems (2026 edition)"
status, headers, body = handle_get(course, '"stale-etag"')
print(status, len(body), "bytes")

Output:

Plaintext
200 "401e4c7a6f35767e" 39 bytes
304 0 bytes
200 54 bytes

The 304 saves bandwidth and serialisation time; for large JSON or HTML responses the savings are significant. Note that the ETag must be built from a stable representation (sort_keys=True) or it will change on every request.

The Vary header

If a response differs by request header — language, encoding, device — tell caches with Vary:

Plaintext
Vary: Accept-Encoding, Accept-Language

Without it, a CDN could serve an Urdu page to an English user. Avoid Vary: Cookie on public pages; it destroys cache reuse.

CDNs: caching at the edge

A Content Delivery Network keeps copies of your content in data centres close to users. A student in Lahore downloads images from a nearby edge server instead of a server on another continent.

How a CDN request works:

  1. The user's DNS lookup routes them to the nearest edge location.
  2. Edge hit → served immediately.
  3. Edge miss → the CDN fetches from your origin server, caches it per your headers, and serves it.

CDN best practices:

  • Fingerprint static files (app.3f9a1c.js) and cache them for a year — deploying a new version changes the file name, so no purge is needed.
  • Use s-maxage to cache HTML at the edge longer than in browsers.
  • Purge by URL or tag when content changes urgently.
  • Never cache authenticated responses publicly — mark them private or bypass the CDN for them.
  • Serve images in modern formats (WebP/AVIF) and correct sizes.

Caching API responses

APIs benefit too:

  • Public, read-only endpoints (course catalogue, exchange rates) can use Cache-Control: public, max-age=… and a CDN.
  • Per-user endpoints should be private and rely on ETags for revalidation.
  • POST, PUT and DELETE responses aren't cached by default.

Common mistakes

  • max-age on HTML without versioning, so users keep seeing an old page after a deploy.
  • Public caching of personalised responses — one user sees another's data.
  • Unstable ETags that change every request, so 304 never happens.
  • Purging the whole CDN on every deploy instead of fingerprinting assets.

Key takeaways

  • Cache-Control decides who may cache and for how long; no-store for sensitive data.
  • ETags and 304 Not Modified avoid re-sending unchanged content.
  • CDNs cache at the edge — fingerprint static assets and cache them for a year.
  • Mark personalised responses private and use Vary correctly.
Finished this lesson?
Tick it off to track your Caching Strategies progress.
Next lesson