October 4, 2026 · Yunus Emre Vurgun
How Do Cache-Control and ETag Headers Work?
Cache-Control tells caches whether and how long they may store a response, while ETag gives each version of a resource a fingerprint so clients can revalidate cheaply instead of re-downloading. Together they cut bandwidth, latency, and server load: fresh cache hits answer in milliseconds with no origin request at all. This guide explains the directives, the validation flow, and the strategies that work for APIs and static assets alike.
Cache-Control: who caches and for how long
Every caching decision starts with the Cache-Control response header. Its directives answer two questions: which caches may store this, and how long the stored copy stays fresh. The essential vocabulary fits in one table:
| Directive | What it means | Typical use |
|---|---|---|
max-age=3600 | Fresh for 3600 seconds after receipt. | Any cacheable response; the workhorse directive. |
public | Any cache may store it, including CDNs. | Public assets, open data, marketing pages. |
private | Only the end user's browser may store it. | Personalized pages and authenticated API responses. |
no-cache | Store it, but revalidate before every reuse. | HTML shells and data that changes unpredictably. |
no-store | Never store any part of the response. | Passwords, tokens, payment and health data. |
must-revalidate | Never serve stale once fresh lifetime ends. | APIs where stale data is worse than an error. |
immutable | This URL's content will never change. | Fingerprinted assets like app.a3f9c1.js. |
Note the classic trap: no-cache does not mean do not cache — it means cache but always revalidate. When you truly want nothing stored, say no-store. Combine directives freely: Cache-Control: public, max-age=31536000, immutable is the standard recipe for versioned static assets, while private, max-age=60, must-revalidate suits a snappy but honest personalized API. The HTTP caching guide dataset keeps the full directive reference.
ETag and conditional requests
An ETag is an opaque version tag the server attaches to a response, such as ETag: "v2-patch3". When the cached copy expires, the client does not download the whole thing again — it sends the tag back in If-None-Match and asks whether anything changed:
GET /static-data/dataset/sql-join-types.json HTTP/1.1
Host: yjtoon.com
If-None-Match: "v2-patch3"
HTTP/1.1 304 Not Modified
ETag: "v2-patch3"
Cache-Control: public, max-age=3600
If the tag still matches, the server answers 304 Not Modified with no body — the client keeps its copy and resets the freshness clock. A 304 is typically a few hundred bytes versus potentially megabytes, so revalidation is nearly free even when it happens on every request. Generate ETags from content hashes for static files, and from a version counter or updated-at timestamp plus content hash for dynamic resources. Strong ETags (the default) promise byte-identical equality; weak ETags prefixed with W/ promise only semantic equivalence, which suits compressed or cosmetically transformed variants.
Last-Modified versus ETag
Last-Modified is the older validation mechanism: the server stamps a timestamp, and the client echoes it in If-Modified-Since. It still works and costs almost nothing to emit, but it has three weaknesses ETags avoid. Timestamps have one-second resolution, so two edits within the same second look identical. Clock skew and timezone handling add failure modes ETags never have. And a file rewritten with identical content gets a new timestamp but can keep its ETag, avoiding a pointless re-download.
The pragmatic answer is to send both headers. Well-behaved clients prefer If-None-Match when an ETag is present and fall back to If-Modified-Since otherwise, and your server should honor both, checking the ETag first. Never send a Last-Modified date in the future — caches treat that as an error signal — and make sure both validators agree, since a mismatched pair confuses intermediaries and defeats revalidation.
Caching strategies for APIs and static assets
Different content wants different policies. For fingerprinted static assets — filenames containing a content hash — cache aggressively: public, max-age=31536000, immutable. The URL changes whenever the bytes change, so a long lifetime can never serve stale content. For HTML entry points and API responses that change on unknown schedules, use short freshness plus validation: max-age=60, must-revalidate with an ETag, so clients stay nearly current at the cost of tiny 304s. For rapidly changing or user-specific data, use private, no-cache so browsers revalidate every time without intermediaries ever seeing the bytes.
CDNs add one more layer: s-maxage overrides max-age for shared caches only, letting you cache longer at the edge than in browsers. Purge or version your URLs on deploy so edge caches cannot pin old content — cache invalidation by timeout alone is how stale APIs linger for hours. If you serve reference data to automated clients, the patterns in caching LLM context with CDN files and the agent-friendly HTTP checklist show how consistent headers make machine consumers faster and cheaper.
Why is my browser showing a stale page?
Stale content almost always traces back to one of four causes. First, an over-long max-age on a URL whose content changed — the fix is fingerprinting assets so URLs change with bytes. Second, missing revalidation: a response with max-age but no ETag or Last-Modified cannot be checked cheaply, so caches either refetch fully or serve stale. Third, an intermediary ignoring your headers: confirm with curl -I that the headers you set are the headers clients receive, and check for a proxy stripping them. Fourth, the Age header reveals how long a shared cache has held its copy — if it exceeds your max-age, something upstream is misconfigured.
Debug methodically: reproduce with curl -sS -D - -o /dev/null to see the exact headers, then hard-refresh to separate browser cache from CDN cache. Keep the complete header reference at http-caching-guide.json nearby while you tune, and change one directive at a time so you can attribute the effect.