gap·map

HTTP & caching

[ junior depth ]

Why caching exists

An HTTP cache can save you at two levels. Either the browser skips the network entirely because it already has a fresh copy, or it skips the body transfer because the server confirms the stored copy is still good with a tiny response. The first is free, the second costs one round trip. Everything in this topic is about controlling which of these happens, for which file, for how long.

What the Cache-Control header does

Cache-Control is a response header. The server declares the policy and the browser obeys automatically:

Cache-Control: max-age=3600

max-age=3600 means the response is fresh for an hour. While fresh, the browser serves it straight from cache and sends no request at all. Fast repeat views come from this, and so does the danger of a wrong max-age: once browsers have stored it, you can't take it back.

The difference between no-cache and no-store

Interviewers use this pair as a trick question:

  • no-store: never store the response. The real "don't cache".
  • no-cache: do store it, but ask the server before every reuse. The name lies; it does not mean "don't cache".

How ETag and 304 revalidation work

How does "ask the server" work without re-downloading? The server tags the response:

ETag: "a1b2c3"

Next time, the browser sends If-None-Match: "a1b2c3". If the file hasn't changed, the server answers 304 Not Modified, a response with no body. You paid one round trip instead of the whole download. (Last-Modified/If-Modified-Since is the older, timestamp-based version of the same exchange.)

How to check caching in DevTools

The Network panel shows the difference. "(disk cache)" or "(memory cache)" means no request happened; status 304 means a cheap revalidation; status 200 with a full size means the cache didn't help. A hard refresh bypasses freshness on purpose, and "Disable cache" turns the whole thing off while the panel is open. Which files should get which policy is the middle-level story.

[ middle depth ]

How to cache HTML vs hashed assets

Almost every production frontend converges on the same split:

# index.html: the entry point
Cache-Control: no-cache          # store, but revalidate every time (+ ETag → 304)

# app.3f9c2b.js, app.9d01e4.css: hashed assets
Cache-Control: max-age=31536000, immutable

Why it works: the URL is the cache key. Hashed filenames change whenever content changes, so a year-long cache can never serve the wrong version; a new deploy references new URLs. immutable goes further and tells the browser not to revalidate even on reload. The HTML is the opposite case. Its URL never changes, so it must be revalidated every time. It is effectively your version manifest, the one file that decides which asset versions users get.

Invert this and long-cache the HTML, and users get pinned to an old app shell referencing old bundles until the cache expires. Browser caches cannot be purged remotely; there is no undo.

When a cached response goes stale

A cached response is fresh until its age exceeds max-age, then stale. Stale does not mean deleted. It means "revalidate before reuse": a conditional request that ends in a cheap 304 (reuse the stored copy, freshness clock reset) or a full 200 (new version). Two details trip people up:

  • The Age header: freshness counts from generation at origin, and time spent in a CDN eats into it.
  • Heuristic caching: if you send no Cache-Control at all, browsers may cache anyway (roughly 10% of the time since Last-Modified). Silence is not "no caching", so always be explicit.

private vs public: who may store the response

private means browser caches only. It is required for personalized or authenticated responses; otherwise a CDN could serve one user's data to another. public allows shared caches even for responses that normally wouldn't qualify. For truly sensitive data, no-store is the only directive that means what people think no-cache means.

Also in the middle-level toolkit: must-revalidate (never serve stale on origin failure, error out instead), strong vs weak ETags (W/"..."), and why ETag beats Last-Modified (sub-second edits, unreliable clocks). What happens above the browser, meaning CDNs, s-maxage, and stale-while-revalidate, is the senior story.

[ senior depth ]

How many caches sit between the user and origin

A response can be stored in the browser's HTTP cache, the back/forward cache, a service worker's Cache API, a CDN edge node, a corporate proxy. Each has its own freshness state, and senior-level reasoning always names which cache a directive targets. The key split: max-age speaks to everyone, while s-maxage overrides it for shared caches only. That unlocks the production pattern of caching long at the edge and short in browsers:

Cache-Control: public, s-maxage=600, max-age=0, stale-while-revalidate=30

CDNs add purge APIs (and vendor headers like CDN-Cache-Control), so edge TTLs are reversible in a way browser TTLs never are. Deploys purge the edge; browsers were never allowed to hold on.

stale-while-revalidate and stale-if-error

stale-while-revalidate=N serves the stale copy instantly and refreshes in the background. Users never wait on revalidation; the next visitor gets the new version. stale-if-error=N keeps serving stale when origin is down, which buys availability from the cache layer. Know where they don't belong: anything transactional (payments, stock counts) where "instant but possibly stale" is a bug, not a feature.

Vary, cache keys, and cache poisoning

Vary extends the cache key with request headers. Vary: Accept-Encoding keeps gzip/brotli variants separate (correct); Vary: User-Agent fragments the cache into thousands of useless entries (anti-pattern). Forgetting to key on something the response depends on is how cache poisoning works: an attacker gets a response built from an unkeyed input stored in the shared cache and served to everyone. A personalized page cached as public belongs to the same family of bugs, where request collapsing then hands one user's account data to the next. When a CDN "leaks" data, the fix almost always lives in the cache keys or a missing private.

Version skew between deploys and cached HTML

Hashed assets plus a revalidated HTML manifest form a contract: assets never change in place, HTML decides versions. The failure mode to design for is the skew window, where a user's old HTML requests assets your deploy just deleted (hard 404s), or ends up with a half-cached mix of old and new chunks. Mitigations: keep N previous asset versions on the server, atomic deploys, SPA reload-on-chunk-404. ETag has senior-level footnotes too. It costs computation, and default multi-server configs (Apache's inode-based tags) generate different ETags per server, which kills your 304 rate behind a load balancer. Debugging "user still sees the old bundle" means walking every layer in order: service worker, browser, edge, origin.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

unlocks

more Browser & Network