HTTP Caching Deep Dive: Cache-Control, ETag, Revalidation
HTTP caching allows browsers and intermediary caches to reuse prior responses instead of downloading the full response every time. When designed well, caching reduces latency, bandwidth, server load, and repeat-visit cost. When designed badly, it causes stale content, broken deploys, inconsistent UI, or even privacy issues.
A strong frontend answer should distinguish between three states:
- Fresh hit β response is reused without contacting the server.
- Stale but revalidated β cache asks server whether stored response is still valid.
- Miss / refetch β full response must be downloaded again.
Modern HTTP caching behavior is primarily driven by Cache-Control, plus validators like ETag and Last-Modified. Vary also matters because caches do not just ask "Do I have this URL?" They ask whether they have the right representation of that URL for the current request.
Quick Navigation: How HTTP Caching Works: Fresh, Stale, Revalidate, Refetch β’ Cache-Control: The Most Important Header β’ Cache Key and Vary: Correctness Before Hit Rate β’ ETag and Last-Modified: Validators for Revalidation β’ Conditional Requests and 304 Not Modified β’ stale-while-revalidate: Fast Feel, Eventual Freshness β’ Freshness Metadata: Age, Expires, and Heuristic Caching β’ Choosing Policies by Resource Type
Quick Decision Guide
Senior-Level Decision Guide:
- Use long-lived immutable caching for versioned static assets like app.abc123.js. - Use short TTL or revalidation for HTML/documents because they coordinate the latest asset graph and page content. - Use ETag or Last-Modified when content may be reused but freshness must be verified after it becomes stale. - Use stale-while-revalidate when slightly stale content is acceptable and you want low perceived latency. - Use no-store for highly sensitive responses that must never be persisted in caches. - Be careful with shared caches: user-specific responses often need private, Vary, or should avoid shared caching altogether. - Remember that correctness comes before hit rate. A wrong cached response is worse than a miss.
Interview framing: HTTP caching is about freshness, validation, reuse, cache key correctness, and safety, not just βmaking things faster.β
How HTTP Caching Works: Fresh, Stale, Revalidate, Refetch
A cached response is usually in one of two states:
The freshness lifetime is often controlled by Cache-Control: max-age=... or sometimes Expires.
Lifecycle
1) First request
The browser requests a resource and receives:
ETag or Last-Modified2) Later request while response is fresh
If the cached response is still fresh, the browser can reuse it directly without contacting the server.
3) Later request after response becomes stale
Once stale, the browser may:
4) Server response after revalidation
The server may return:
Lifecycle diagram
User request
|
v
Browser cache lookup
|
|-- Fresh -> Serve cached response
|
|-- Stale + validator
| |
| v
| Conditional request
| |
| |-- 304 -> reuse cached body
| |-- 200 -> replace cached body
|
|-- Miss -> fetch full responseKey Interview Point
A 304 is cheaper than downloading the whole payload, but it is not free. It still costs a network round trip and server participation. The best repeat-access outcome is usually a fresh cache hit, not just successful revalidation.
Cache-Control: The Most Important Header
Cache-Control is the primary mechanism for controlling caching behavior. It defines whether a response can be stored, how long it stays fresh, and what private or shared caches are allowed to do.
Core Directives
`max-age=SECONDS`
How long the response stays fresh.
Cache-Control: public, max-age=3600Meaning:
`s-maxage=SECONDS`
Like max-age, but specifically for shared caches such as CDNs or proxies.
Cache-Control: public, max-age=60, s-maxage=3600Meaning:
`public`
Response may be stored by shared caches like CDNs or proxies.
`private`
Response is intended for a single user and should not be stored by shared caches.
This is important for authenticated or personalized responses.
`no-cache`
Commonly misunderstood.
no-cache does not mean βdo not cache.β It means the response may be stored, but it must be revalidated before reuse.
`no-store`
Do not store this response in any cache.
Use for highly sensitive content.
`must-revalidate`
After response becomes stale, cache must validate before reuse.
`immutable`
Tells the client that the response body is not expected to change while it is fresh.
This is especially useful for content-hashed static assets.
Cache-Control: public, max-age=31536000, immutable`stale-while-revalidate=SECONDS`
Allows a stale response to be served for an additional time window while the cache revalidates in the background.
Cache-Control: max-age=60, stale-while-revalidate=300Meaning:
Senior Insight
no-cache is about mandatory validation, while no-store is about forbidding storage entirely.
max-age controls freshness for the cache reading the response, while s-maxage lets you give shared caches a different policy from browsers.
Cache Key and Vary: Correctness Before Hit Rate
A cache does not just store βa URL.β It stores a response associated with a request.
In practice, cache reuse depends on a cache key, which often includes:
The Vary header tells caches which request headers affect the representation.
Example
Vary: Accept-EncodingThis means the compressed and uncompressed forms are different cached variants.
Other common examples
Vary: Accept-Language
Vary: Origin
Vary: Accept-EncodingWhy this matters
If Vary is missing or wrong, a cache may reuse the wrong representation.
Example bug:
Auth and personalization warning
If a response depends on authentication, cookies, or user identity, shared caching must be handled very carefully.
Options often include:
Cache-Control: privateInterview framing
Hit rate is good, but correctness is non-negotiable. The purpose of Vary is to prevent the cache from serving the right URL with the wrong content.
ETag and Last-Modified: Validators for Revalidation
Validators help the client ask: βHas this resource changed since the version I already have?β
ETag
ETag is an opaque identifier representing a specific version of a resource.
ETag: "v3-9f1ab"On the next request, the browser can send:
If-None-Match: "v3-9f1ab"If the resource has not changed, the server returns:
304 Not Modifiedand the browser reuses the cached body.
Last-Modified
Last-Modified stores a timestamp for the resource version.
Last-Modified: Wed, 12 Mar 2026 10:00:00 GMTThe browser can later send:
If-Modified-Since: Wed, 12 Mar 2026 10:00:00 GMTIf unchanged, the server can return 304 Not Modified.
ETag vs Last-Modified
ETag
Last-Modified
Senior Insight
For static build assets, validators matter less when filenames are content-hashed because URL changes already invalidate cache safely. Validators are especially useful for:
Conditional Requests and 304 Not Modified
Conditional requests let the client avoid downloading the full response if its cached version is still valid.
Typical Flow
Initial response
HTTP/1.1 200 OK
Cache-Control: max-age=0, must-revalidate
ETag: "article-42-v7"Later request
GET /article/42
If-None-Match: "article-42-v7"Server response if unchanged
HTTP/1.1 304 Not ModifiedWhy This Is Useful
Why This Is Not the Best Possible Case
Interview Framing
A mature answer sounds like:
stale-while-revalidate: Fast Feel, Eventual Freshness
stale-while-revalidate extends the usefulness of a cached response after it becomes stale. Instead of blocking the user on revalidation, a cache may serve the stale response immediately and refresh it in the background.
Example
Cache-Control: max-age=60, stale-while-revalidate=300What this means
Best Use Cases
Bad Use Cases
Senior Insight
This directive is powerful because it improves perceived latency without fully sacrificing freshness. But it is a product decision, not just a performance toggle. You must know whether temporary staleness is acceptable.
Freshness Metadata: Age, Expires, and Heuristic Caching
Caching is not only about max-age.
`Age`
Age tells how long the response has already spent in a cache.
This matters especially with shared caches and CDNs.
`Expires`
Expires is an older absolute timestamp after which the response is stale.
If both Cache-Control and Expires are present, modern caches generally prefer Cache-Control.
Heuristic caching
If no explicit freshness information is present, caches may sometimes estimate a freshness lifetime heuristically, often using metadata such as Last-Modified.
Interview takeaway
Do not assume βno Cache-Control means no caching.β Some caches can still reuse a response heuristically, which is one reason explicit cache policy is safer than accidental defaults.
Choosing Policies by Resource Type
A senior engineer does not use one caching policy for everything.
1) Versioned static assets
Examples:
app.7e41c9.jsstyles.a82fd1.cssRecommended:
Cache-Control: public, max-age=31536000, immutableWhy:
2) HTML documents
Recommended often:
Cache-Control: public, max-age=0, must-revalidateor a short freshness window when appropriate.
Why:
3) API responses
Depends on data type:
private, sometimes no-store4) Authenticated/private responses
Use caution with shared caches.
Typical choice:
Cache-Control: private, no-cacheor stronger restrictions if persistence is unsafe.
5) Images and media
Often safe for longer caching if URL versioning exists.
Senior Insight
The real skill is mapping resource semantics to cache semantics:
Cache Busting and Why Hashed Filenames Matter
Cache busting means changing the resource URL when content changes so old cached copies are no longer used.
Typical Approach
Instead of:
/app.jsUse:
/app.7e41c9.jsWhen content changes, build produces a new filename:
/app.d2f8b1.jsNow caches treat it as a different resource.
Why This Is Powerful
Interview Insight
Hashed filenames are one of the most important frontend deployment patterns because they let you combine:
Without versioned URLs, long cache lifetimes are risky because users may keep outdated files after a release.
Common Interview Traps
Trap 1: βno-cache means donβt cacheβ
Wrong.
It means the response may be stored, but it must be revalidated before reuse.
Trap 2: β304 means no network requestβ
Wrong.
It still requires a request/response exchange.
Trap 3: βETag is only for APIsβ
Wrong.
It can validate any stable-URL resource whose representation may change.
Trap 4: βCache-Control is only for browsersβ
Wrong.
HTTP caching semantics also affect intermediaries such as proxies and CDNs.
Trap 5: βOne caching strategy fits the whole appβ
Wrong.
Different resources need different freshness and privacy guarantees.
Trap 6: βService worker cache and HTTP cache are the same thingβ
Wrong.
They are related but distinct layers.
Trap 7: βIf URL matches, cached response is safe to reuseβ
Wrong.
Vary and cache key correctness determine whether it is the right representation.
Interview Scenarios
Scenario 1: JS bundles in production
Question: How would you cache JavaScript bundles?
Strong answer:
Scenario 2: Product listing API
Question: Data changes a few times per hour. What would you do?
Strong answer:
max-agestale-while-revalidateScenario 3: Authenticated dashboard
Question: Should this be cached?
Strong answer:
private and often revalidation or even no-store for very sensitive stateScenario 4: News homepage HTML
Question: Should HTML be long-cached?
Strong answer:
Scenario 5: Slow repeat loads despite caching
Question: Why might that happen?
Strong answer:
no-cache may force network checksScenario 6: Wrong language served from cache
Question: What likely happened?
Strong answer:
Vary: Accept-LanguageScenario 7: Personalized content leaked via CDN
Question: What likely went wrong?
Strong answer:
Vary policy did not safely separate representationsBest Practices
1. Differentiate by resource type. HTML, APIs, and static assets should rarely share the same policy.
2. Use hashed filenames for deploy-safe asset caching. This enables aggressive long-term caching without stale deploy issues.
3. Prefer fresh hits over constant revalidation when safe. Revalidation is useful, but a true fresh hit is cheaper.
4. Use validators for stable URLs that may change. ETag and Last-Modified are especially useful for documents and APIs.
5. Use private or stronger controls for user-specific responses. Never accidentally let personalized responses leak through shared caches.
6. Use Vary carefully. Cache correctness is more important than maximizing reuse.
7. Use stale-while-revalidate only when slight staleness is acceptable. It improves perceived speed but is a product trade-off.
8. Treat caching as part of release engineering. Bad cache policy can break deployments just as easily as bad code.