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.
| Directive | Effect |
|---|---|
public | any cache may store it, even with Authorization present |
private | only the browser may store it, never a CDN or shared proxy |
no-cache | store it, but revalidate before every reuse |
no-store | never 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-revalidate | once stale, revalidate, do not serve stale on error |
proxy-revalidate | must-revalidate for shared caches only |
immutable | never revalidate while fresh, even on a reload |
no-transform | proxies 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
| Directive | Effect |
|---|---|
no-cache | force revalidation, what a browser hard reload sends |
no-store | do not store this exchange |
max-age=0 | treat 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-cached | serve 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>
| Validator | Strength | Notes |
|---|---|---|
ETag: "abc" | strong, byte-for-byte identical | required for range requests |
ETag: W/"abc" | weak, semantically equivalent | fine for revalidation, not for ranges |
Last-Modified | weak, 1-second resolution | cannot 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.
| Situation | Correct move |
|---|---|
| Compressed variants | Vary: Accept-Encoding |
| Localized content | Vary: Accept-Language, or separate URLs |
| Per-user content | Cache-Control: private, never Vary: Cookie on a CDN |
| A/B variants | separate 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
| Layer | Honors | Notes |
|---|---|---|
| Browser | max-age, no-store, immutable | a hard reload sends Cache-Control: no-cache |
| Service worker | your code only | it can ignore HTTP semantics entirely |
| Shared proxy | public, s-maxage, private | will not store private |
| CDN | s-maxage first, then max-age | plus 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