HTTP Caching Explained

Lab
Easy
Medium
Hard

API response, max-age=60Medium

Fast repeat calls — but a change at t=40 isn't seen until the copy expires.

Response headers from the server

Cache-Control: private, max-age=60
ETag: "v1"
max-age
stale-while-revalidate
Audience
1 / 6t = 0s
Miss → 200
First request → 200. Nothing stored yet. The response is stored for next time.

What the browser cache does

time runs left → right
0s
visit
30s
visit
40s
deploy
50s
visit
1.5m
visit
1.7m
visit
cached copy is fresh server content changes
How the browser decided
  1. ✗Cache-Control: no-store? No — the response may be stored
  2. ✗Is there a stored copy? No — first visit
Browser sends
GET /app.js
—
Server replies
200 OK
Cache-Control: private, max-age=60
ETag: "v1"
Data transferred so far120 KB vs 120 KB with no cache

Whole timeline: 60% less data than caching nothing · 3 served without a full download · 1 time the user saw outdated content

Learn the theory

HTTP Caching Explained: how it works

4 min read · Updated

The fastest request is the one you never send. HTTP caching lets browsers and CDNs reuse earlier responses, saving bandwidth and shaving hundreds of milliseconds off every page view. It is controlled almost entirely by a handful of response headers — and getting them wrong is why users sometimes see a stale site for days after a deploy, or why your API gets hammered by requests that should have been cached.

In short

  • A cached response is either fresh (reuse it with no network request) or stale (it must be revalidated or replaced).
  • Cache-Control: max-age=N sets how many seconds a response stays fresh.
  • Validators (ETag, Last-Modified) let the browser ask “has it changed?” and get a tiny 304 Not Modified instead of the whole body.
  • no-cache means “store it, but always revalidate”. no-store means “never keep it”. They are not the same.
  • For static files use content-hashed filenames with a one-year immutable cache; for HTML use no-cache with an ETag.

Freshness and validation

Every cached response goes through two questions. First: is it still fresh? If the response's age is below its max-age, the browser serves it directly from the cache — no network, no latency. Second, once it's stale: is it still valid? The browser can send a conditional request asking the server whether the stored copy still matches the current resource.

# First request
GET /app.js HTTP/1.1

HTTP/1.1 200 OK
Cache-Control: max-age=60
ETag: "v1"
(120 KB body)

# After 60 s the copy is stale -> conditional request
GET /app.js HTTP/1.1
If-None-Match: "v1"

HTTP/1.1 304 Not Modified      <- ~350 bytes, no body
Cache-Control: max-age=60      <- freshness timer restarts

Cache-Control directives you need

DirectiveMeaning
max-age=NFresh for N seconds after the response was generated.
s-maxage=NLike max-age, but only for shared caches (CDNs, proxies).
no-cacheMay be stored, but must be revalidated with the server before every reuse.
no-storeMust not be stored anywhere. Use for sensitive data.
must-revalidateOnce stale, don't serve it without revalidating (even if the network is down).
public / privateprivate = only the user's browser may cache it; public = shared caches may too.
immutableThe file will never change while fresh; don't revalidate it, even on reload.
stale-while-revalidate=NFor N seconds after expiry, serve the stale copy instantly and refresh in the background.
stale-if-error=NServe stale content if the origin errors.

ETag and Last-Modified

Validators identify a specific version of a resource. An ETag is an opaque fingerprint (often a content hash) compared with If-None-Match. Last-Modified is a timestamp compared with If-Modified-Since. ETags are more precise; Last-Modified has only one-second resolution. If a response has neither a max-age nor a validator, browsers can't safely reuse it.

Reload, hard reload and the back button

User actionWhat the browser does
Click a link / type a URLUses fresh cache entries without asking the server.
Back / forward buttonReuses cached pages aggressively, even stale ones.
Reload (F5)Revalidates cached entries with conditional requests (unless immutable).
Hard reload (Ctrl/⌘+Shift+R)Bypasses the cache entirely and downloads everything again.

Recipes that work

ResourceRecommended headerWhy
Hashed static assets (app.3f9c1.js)public, max-age=31536000, immutableThe URL changes whenever the content does, so cache it for a year.
HTML documentsno-cache + ETagAlways check, but pay only for a 304 when nothing changed.
API responses (per user)private, max-age=0 or short max-age + ETagNever let a shared cache serve one user's data to another.
Public API / imagespublic, max-age=300, stale-while-revalidate=3600Fast for users, gentle on the origin.
Sensitive pagesno-storeNothing stays on disk or in proxies.

CDNs, shared caches and Vary

A CDN is a shared cache sitting between users and your server. s-maxage lets you give the CDN a different lifetime than browsers, and private keeps personalized responses out of it. If a response differs by request header — language, compression, Origin for CORS — add a Vary header (for example Vary: Accept-Encoding, Origin) so caches store separate variants instead of serving the wrong one.

Frequently asked questions

What is the difference between no-cache and no-store?
no-cache lets a cache store the response but forces revalidation before every reuse. no-store forbids storing it at all.
What does a 304 Not Modified response mean?
The server is telling the browser that its cached copy is still current. The 304 has no body, so only headers cross the network, and the cached body is reused.
How does ETag work?
The server sends an ETag identifying the version of the resource. On later requests the browser sends it back in If-None-Match; if it still matches, the server replies 304 instead of resending the body.
Why do users still see the old version of my site after a deploy?
Their browser (or a CDN) is serving a cached copy that is still fresh under its max-age. Use content-hashed asset filenames and no-cache on the HTML so new deploys are picked up immediately.
What is stale-while-revalidate?
A directive that lets a cache serve an expired response immediately while it fetches a fresh copy in the background, giving users instant responses without permanently serving stale data.
Does a hard reload clear the cache?
It bypasses the cache for that load and replaces the stored copies with fresh responses. A normal reload instead sends conditional requests.

Keep exploring

More interactive labs

All labs

CORS & Preflight Simulator

Edit the request and the server headers, then see exactly why the browser blocks it.

Networking12 min · Intermediate

What Happens When You Type a URL

DNS, TCP, TLS and HTTP — with a stopwatch.

Networking10 min · Beginner

JS Event Loop Visualizer

See the call stack, microtasks and timers run, frame by frame.

JavaScript10 min · Intermediate

How Database Indexes Work (B-Tree)

See a B-tree lookup beat a full table scan, and why some queries ignore your index.

Databases12 min · Intermediate

What Happens When 100 Reads Hit the Database

Connection pools, CPU workers, caches and replicas, as 100 requests arrive at once.

Databases12 min · Intermediate

SQL Isolation Levels Lab

Run two transactions side by side and watch dirty reads, phantoms and write skew appear.

Databases15 min · Advanced

Sharding & Replication

Split data across servers, copy it for reads, and see what each choice costs.

Databases15 min · Advanced

Redis Commands & Data Structures

Strings, lists, hashes, sets and sorted sets, one command at a time.

Redis15 min · Beginner

Redis Caching: Cache-Aside, TTL & Eviction

Hit or miss? Watch LRU, LFU, TTLs and a cache stampede decide your database load.

Redis12 min · Intermediate

Webhooks: Delivery, Retries & Signatures

Why you get duplicate deliveries, lost events and forged requests — and how to stop them.

Queues & Jobs12 min · Intermediate

Cron Jobs: Schedules, Overlaps & Missed Runs

Write a cron expression and watch what really happens when jobs overlap or the scheduler is down.

Queues & Jobs10 min · Beginner

Job Queues with BullMQ (Redis)

Watch jobs move through waiting, active, delayed, completed and failed — and see what BullMQ stores in Redis.

Queues & Jobs14 min · Intermediate

RabbitMQ: Exchanges, Queues, Acks & Dead Letters

Direct, topic and fanout routing, competing consumers, prefetch, redelivery and dead-letter queues, drawn.

Queues & Jobs14 min · Intermediate

Git Commands, HEAD & Refs (Visual)

Every core git command drawn with arrows: three areas, commits, branches, HEAD and remotes.

Git15 min · Beginner

Git Merge vs Rebase

Watch the commit graph change under merge, rebase and squash.

Git10 min · Beginner