JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

HTTP Caching, Proxies, and the 304

The fastest request is the one you never send. This guide shows how a copy kept close by lets the web skip whole round trips, how the 304 lets you reuse an old copy for free, and where proxies sit to share those copies across a crowd.

The cheapest request is no request

The previous guide left you with one stark fact: distance and round trips dominate how fast the web feels, and you cannot beat the speed of light. If a page needs eighty small files and each one costs a round trip to a server an ocean away, the page is slow no matter how fat your link is — remember that bandwidth, throughput, and latency are three different things, and more bandwidth does nothing for the wait. So the most powerful trick on the whole web is not to send faster. It is to not send at all. If your browser already holds a perfectly good copy of that logo from yesterday, the fastest possible response is the one it can answer itself, in zero milliseconds, without touching the network.

This is caching: keep a copy of something close to where it is used, so you can reuse the copy instead of re-fetching the original. You have already met the idea twice on this ladder. DNS caches name-to-address answers so a repeat lookup skips the walk from the root. Switches cache where MAC addresses live so they stop flooding every frame. Web caching is the same instinct applied to the things HTTP carries — web pages, images, scripts, stylesheets, video chunks — and it is the foundation everything else in this rung builds on, including the content delivery network you will meet next.

Fresh enough? Cache-Control and the expiry date

A cache is useless if it cannot tell when its copy has gone stale, so the server must say. It does this with a response header. The key one is Cache-Control, and the most important piece of it is a max-age, a number of seconds the copy may be reused without checking back. A response carrying Cache-Control: max-age=3600 is the server saying, in effect, 'this is good for an hour; serve it straight from your cache and do not bother me until then.' Think of it as a printed best-before date stamped on the carton: while the date is in the future, the cache pours the milk without phoning the dairy.

While a copy is still within its max-age it is called fresh, and a request that a cache can satisfy from a fresh copy is a cache hit — no network, no round trip, instant. Once the seconds run out the copy is stale: not necessarily wrong, just no longer trusted on its own. The cache must now check whether the original has changed before reusing it. Cache-Control also carries the on/off switches: no-store means 'never keep a copy of this at all' (the right answer for that bank balance), and private means 'only the user's own browser may cache this, not a shared proxy in the middle' (the right answer for anything personalised).

The 304: reusing a stale copy for free

When a copy goes stale, the obvious move is to download it again — but that often wastes the whole transfer, because nine times out of ten the file has not actually changed. HTTP has a beautiful shortcut for exactly this. Instead of asking 'send me the file,' the cache asks 'send me the file ONLY if it differs from the copy I already hold.' This is a conditional request, and when the answer is 'no change,' the server replies with a tiny status code — 304 Not Modified — and no body at all. The cache then serves its existing copy. You paid for one small round trip instead of a full re-download.

How does the cache phrase 'the copy I already hold'? With a validator the server handed it earlier. The strong one is the ETag — an opaque tag, often a hash of the content, that the server attaches when it first sends the file (ETag: "9f3c1"). On revalidation the cache sends it back in an If-None-Match header: 'I have version 9f3c1; still current?' If the server's current ETag matches, it returns 304; if it differs, it returns 200 with the new body and a new ETag. A weaker validator is the Last-Modified date, echoed back in If-Modified-Since, but ETags are preferred because content can change without its timestamp telling a clean story.

First visit (cache is empty):
  GET /logo.png                          -->
  <-- 200 OK   Cache-Control: max-age=86400   ETag: "9f3c1"   [12 KB body]

Next day (copy is now stale, ask: did it change?):
  GET /logo.png   If-None-Match: "9f3c1"  -->
  <-- 304 Not Modified   (no body)        ...cache serves its own 12 KB copy

If it HAD changed:
  <-- 200 OK   ETag: "a7d20"   [new 12 KB body]
A conditional request and the 304. The first fetch costs a full download; every later check costs one tiny round trip and usually a near-empty 304, so the 12 KB body crosses the network just once until the image actually changes.

Notice the honest limit: a 304 still costs one round trip. It saves the bytes of the body, which is huge for a big file, but it does not save the latency of asking. That is why a far-future max-age (the fingerprinted-filename trick) beats revalidation when you can use it: a fresh hit costs zero round trips, while a 304 costs one. Caching has two distinct savings — skipping the bytes, and skipping the trip — and only a fresh cache hit gives you both.

Proxies: who keeps the shared copy?

So far the cache has lived inside your own browser, which only ever helps you. But the assumption that powers caching — the same things asked for over and over — is even stronger across a whole crowd. If a thousand people in one office all load the same news site, why should each download it separately? Put one shared cache on the path between them and the wider internet, and the first request fills it while the next nine hundred ninety-nine hit it. That shared middle-box is a proxy: a server that stands between clients and origin servers, relaying requests and, helpfully, caching what passes through.

There are two kinds, and the difference is whose side it stands on. A forward proxy sits in front of the clients and acts on their behalf — the office or campus deploys it, the wider web has no idea it exists, and to the outside it looks as if all requests come from one place. A reverse proxy sits in front of the servers and acts on their behalf — the website operator deploys it, clients think they are talking straight to the site, and behind it the proxy can cache responses, spread load across many back-end machines, and terminate the HTTPS encryption. Same machine in the middle; the label just says which end it represents.

Picture both as the same box with opposite allegiance. In the forward case, clients point at the proxy and the proxy reaches out into the whole internet on their behalf, so the office or campus deploys it and the web cannot see it. In the reverse case, clients reach what they think is the site but really meet the proxy, which relays to the origin servers behind it, so the site deploys it and clients believe the proxy is the site. Identical machinery; only the side it represents changes.

The reverse proxy is the one to hold onto, because it is the seed of the next guide. A reverse proxy that caches content and sits close to users, run by the site (or a service the site pays), is exactly an edge cache. Scatter thousands of such caches across the planet, each holding copies of a site's content near a different city, and you have built the chain of local warehouses for the web that the next guide is entirely about. Everything you just learned about Cache-Control, ETags, and the 304 is the protocol these warehouses speak to stay stocked and stay current.

Putting it together, and the traps

Let us walk one image through the whole machine, from your browser out to a shared cache and back, so the pieces click into place.

  1. Your browser needs /logo.png. It checks its own cache first. If it holds a fresh copy (still inside max-age), it serves that with zero round trips and the story ends here — the cheapest outcome.
  2. No fresh copy, so it sends the request. The request meets a shared cache (a proxy or a CDN edge) on the way. If that cache holds a fresh copy, it answers immediately — you never reach the distant origin at all.
  3. The shared cache holds only a STALE copy. It does not blindly re-download; it sends a conditional request to the origin with If-None-Match and the ETag it stored.
  4. The origin compares ETags. If unchanged it returns a 304 with no body, and the cache refreshes its timer and serves the copy it already had. If changed it returns 200 with the new bytes and a new ETag, which the cache stores and forwards.

Two honest traps to close on. First, a shared cache must never hand one user's private data to another — so personalised or logged-in responses must be marked private or no-store, or you get the classic, frightening bug of someone seeing a stranger's account page. The Vary header exists to keep responses separated by things like language or encoding for the same reason. Second, caching is famously hard to undo: once a copy with a long max-age is fresh out in the world, you cannot reach into a million browsers and a thousand edge caches to delete it. It will be reused until it expires. That is precisely why the fingerprinted-filename trick is so loved — you do not invalidate the old copy, you simply publish a new URL and let the old one age out untouched.