HTTP Cheatsheet

Caching

Use this HTTP reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Cache-Control response directives

The one header that decides everything. It overrides Expires wherever both appear.

DirectiveEffect
publicany cache may store it, even with Authorization present
privateonly the browser may store it, never a CDN or shared proxy
no-cachestore it, but revalidate before every reuse
no-storenever write it to disk or memory anywhere
max-age=<s>fresh for this many seconds
s-maxage=<s>same, for shared caches only, overrides max-age there
must-revalidateonce stale, revalidate, do not serve stale on error
proxy-revalidatemust-revalidate for shared caches only
immutablenever revalidate while fresh, even on a reload
no-transformproxies must not recompress or resize
stale-while-revalidate=<s>serve stale immediately, refresh in the background
stale-if-error=<s>serve stale if the origin errors

no-cache does not mean do not cache. It means cache it and always check first. The directive that actually forbids storage is no-store.

Cache-Control request directives

DirectiveEffect
no-cacheforce revalidation, what a browser hard reload sends
no-storedo not store this exchange
max-age=0treat everything as stale, revalidate
max-stale=<s>accept a response stale by up to this much
min-fresh=<s>require this much remaining freshness
only-if-cachedserve from cache or return 504, never hit the network

Recipes by resource type

# fingerprinted static asset: /app.4f2a91.js
Cache-Control: public, max-age=31536000, immutable

# HTML shell that must reflect deploys immediately
Cache-Control: no-cache
ETag: "b71c04"

# authenticated API response, per-user
Cache-Control: private, no-store

# public API response, cheap to cache at the edge only
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=60

# anything with a token, a password, or PII
Cache-Control: no-store

# expensive public page, tolerate staleness during an outage
Cache-Control: public, max-age=60, stale-while-revalidate=600,
               stale-if-error=86400

The pattern behind all of it: long cache on immutable URLs, no cache on mutable URLs. Never version an asset by query string alone, put the hash in the filename, and let the HTML that references it be the only thing that must stay fresh.

Freshness, staleness, and Age

A cached response is fresh while age < max-age. Age is the seconds it has already spent in caches, and any intermediary increments it.

current_age  = Age + time since it was received locally
freshness    = max-age (or s-maxage in a shared cache)
fresh if     current_age < freshness

Without max-age or Expires, caches may apply a heuristic freshness of roughly 10% of the time since Last-Modified. That is how a page you never configured ends up cached for hours, so always be explicit.

curl -sI https://example.com/app.js | grep -iE 'cache-control|age|etag|expires'
curl -sI https://example.com/ | grep -i 'x-cache\|cf-cache-status\|x-vercel-cache'

Edge status headers are vendor specific but consistent in meaning: HIT served from cache, MISS fetched from origin, STALE served old while refreshing, BYPASS or DYNAMIC not cacheable.

Revalidation with ETag and Last-Modified

1. First response
   200 OK
   Cache-Control: no-cache
   ETag: "9c1f2a"
   Last-Modified: Tue, 15 Sep 2026 10:04:11 GMT
   <full body, 146 KB>

2. Reuse attempt
   GET /app.js
   If-None-Match: "9c1f2a"
   If-Modified-Since: Tue, 15 Sep 2026 10:04:11 GMT

3a. Unchanged
   304 Not Modified          ← headers only, zero body bytes
   ETag: "9c1f2a"

3b. Changed
   200 OK
   ETag: "af0e77"
   <full body>
ValidatorStrengthNotes
ETag: "abc"strong, byte-for-byte identicalrequired for range requests
ETag: W/"abc"weak, semantically equivalentfine for revalidation, not for ranges
Last-Modifiedweak, 1-second resolutioncannot express two edits in one second

If-None-Match takes precedence over If-Modified-Since when both are sent. A 304 must not carry a body, and it should repeat the validators so the cache can refresh its own metadata.

Cache keys, Vary, and invalidation

The default cache key is method plus the full URL. Vary adds request headers to that key.

SituationCorrect move
Compressed variantsVary: Accept-Encoding
Localized contentVary: Accept-Language, or separate URLs
Per-user contentCache-Control: private, never Vary: Cookie on a CDN
A/B variantsseparate URLs, or vary on a normalized custom header
Vary: *uncacheable, equivalent to giving up

Invalidation options, in order of preference:

1. Change the URL. Content-hashed filenames make invalidation unnecessary. 2. Short s-maxage plus stale-while-revalidate. The edge refreshes without ever making a user wait. 3. Explicit purge. Every CDN has an API, and it is the slowest, least reliable path. Do not build a system that depends on it.

Unsafe methods invalidate the cached entry for their target URL, so a successful POST, PUT, PATCH, or DELETE to /orders/8814 evicts the cached GET for that same URL, but not for /orders.

Layers, and how they interact

browser memory → browser disk → service worker → ISP/corporate proxy
→ CDN edge → CDN shield → reverse proxy → application cache → database
LayerHonorsNotes
Browsermax-age, no-store, immutablea hard reload sends Cache-Control: no-cache
Service workeryour code onlyit can ignore HTTP semantics entirely
Shared proxypublic, s-maxage, privatewill not store private
CDNs-maxage first, then max-ageplus vendor headers and its own key config

Common mistakes worth checking for by hand:

- no-cache written where no-store was meant, so tokens sit in the disk cache. - Missing Vary: Accept-Encoding, so a Brotli body reaches a client that cannot decode it. - A long max-age on unversioned HTML, so a deploy takes a day to appear. - Set-Cookie on a public cached response, which leaks one user's session to everyone who hits the same edge node. This is the worst one, and it is a real outage class.

# prove the browser and edge behaviour of one asset
curl -sI https://example.com/app.4f2a91.js
curl -sI -H 'if-none-match: "9c1f2a"' https://example.com/app.js | head -1
curl -sI -H 'cache-control: no-cache' https://example.com/ | grep -i cache