HTTP Caching Deep Dive: Cache-Control, ETag, Revalidation

Easyβ€’

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:

  1. Fresh hit β†’ response is reused without contacting the server.
  2. Stale but revalidated β†’ cache asks server whether stored response is still valid.
  3. 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 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:

β€’Fresh: it can be reused immediately.
β€’Stale: it has expired and may require validation before reuse.

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:

β€’response body
β€’caching headers
β€’possibly validators like ETag or Last-Modified

2) 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:

β€’revalidate with the server using conditional headers, or
β€’refetch the full resource, depending on policy and available validators

4) Server response after revalidation

The server may return:

β€’304 Not Modified β†’ cached body can be reused
β€’200 OK with a new body β†’ cache updates stored response

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 response

Key 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=3600

Meaning:

β€’response can be reused for 1 hour without contacting the server

`s-maxage=SECONDS`

Like max-age, but specifically for shared caches such as CDNs or proxies.

Cache-Control: public, max-age=60, s-maxage=3600

Meaning:

β€’browser cache freshness: 60 seconds
β€’CDN/shared cache freshness: 1 hour

`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=300

Meaning:

β€’first 60 seconds: fresh
β€’next 300 seconds: stale response can still be served while refresh happens in background

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:

β€’URL
β€’request method
β€’selected request headers

The Vary header tells caches which request headers affect the representation.

Example

Vary: Accept-Encoding

This means the compressed and uncompressed forms are different cached variants.

Other common examples

Vary: Accept-Language
Vary: Origin
Vary: Accept-Encoding

Why this matters

If Vary is missing or wrong, a cache may reuse the wrong representation.

Example bug:

β€’first request gets English content
β€’second request should receive French content
β€’cache incorrectly reuses English response

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: private
β€’avoiding shared caching
β€’ensuring the cache key safely varies for the representation

Interview 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 Modified

and 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 GMT

The browser can later send:

If-Modified-Since: Wed, 12 Mar 2026 10:00:00 GMT

If unchanged, the server can return 304 Not Modified.

ETag vs Last-Modified

ETag

β€’more precise
β€’good when content changes frequently or modification time is not enough
β€’useful when the URL stays stable but body may change

Last-Modified

β€’simpler
β€’timestamp-based
β€’less precise than a content/version identifier

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:

β€’HTML documents
β€’API responses
β€’CMS-driven content
β€’resources whose URL stays stable while their body may change

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 Modified

Why This Is Useful

β€’avoids retransmitting large response body
β€’preserves bandwidth
β€’keeps content fresh

Why This Is Not the Best Possible Case

β€’still incurs request latency
β€’still hits server or intermediary
β€’still adds work compared with serving a fresh cached hit

Interview Framing

A mature answer sounds like:

β€’β€œRevalidation is good, but a true fresh hit is cheaper than 304 because it avoids the round trip entirely.”

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=300

What this means

β€’0 to 60 seconds: response is fresh
β€’61 to 360 seconds: stale response may still be used immediately while cache refreshes in background
β€’after that window: normal revalidation/refetch behavior applies

Best Use Cases

β€’content that changes, but not every second
β€’APIs where slightly stale data is acceptable
β€’pages where perceived speed matters more than second-by-second freshness

Bad Use Cases

β€’personalized account balances
β€’highly sensitive state
β€’rapidly changing values where stale output is misleading

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.js
β€’styles.a82fd1.css
β€’fonts and images with fingerprinted names

Recommended:

Cache-Control: public, max-age=31536000, immutable

Why:

β€’hashed filename changes when content changes
β€’extremely safe to cache aggressively
β€’avoids repeat downloads after deploy

2) HTML documents

Recommended often:

Cache-Control: public, max-age=0, must-revalidate

or a short freshness window when appropriate.

Why:

β€’HTML points to the latest asset graph
β€’deploys often need users to see updated document quickly
β€’stale HTML with new/deleted asset references can cause broken deploys

3) API responses

Depends on data type:

β€’public catalog data β†’ cacheable with TTL or stale-while-revalidate
β€’user-specific data β†’ usually private, sometimes no-store
β€’highly volatile state β†’ short TTL or no caching

4) Authenticated/private responses

Use caution with shared caches.

Typical choice:

Cache-Control: private, no-cache

or 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:

β€’immutable assets β†’ long cache
β€’HTML β†’ short freshness / revalidate
β€’user-private state β†’ private or no-store
β€’public semi-static data β†’ TTL + revalidate

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.js

Use:

β€’/app.7e41c9.js

When content changes, build produces a new filename:

β€’/app.d2f8b1.js

Now caches treat it as a different resource.

Why This Is Powerful

β€’allows long-lived caching safely
β€’avoids fragile manual invalidation
β€’deploys become more reliable
β€’reduces unnecessary network traffic for unchanged assets

Interview Insight

Hashed filenames are one of the most important frontend deployment patterns because they let you combine:

β€’very aggressive caching for static assets
β€’safe invalidation on deploy

Without versioned URLs, long cache lifetimes are risky because users may keep outdated files after a release.

Browser Cache vs Shared Cache vs Service Worker Cache

These are related but different layers.

Browser cache

Built into the browser and controlled automatically by HTTP caching semantics.

Shared cache

Examples:

β€’CDN
β€’reverse proxy
β€’intermediary cache

Shared caches may serve many users, which is why public, private, s-maxage, and Vary matter so much.

Service worker cache

Managed by JavaScript using the Cache Storage API.

This is not the same as the browser's built-in HTTP cache.

Mental model

Browser request
     |
Service Worker (optional)
     |
HTTP cache
     |
Network / CDN / origin

Interview trap

Service worker caching and HTTP caching can complement each other, but they are not identical systems.

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:

β€’fingerprint asset filenames
β€’serve with long-lived immutable caching
β€’update HTML quickly so it references latest asset graph

Scenario 2: Product listing API

Question: Data changes a few times per hour. What would you do?

Strong answer:

β€’moderate max-age
β€’possibly stale-while-revalidate
β€’validators for conditional requests
β€’acceptable if users briefly see slightly stale data

Scenario 3: Authenticated dashboard

Question: Should this be cached?

Strong answer:

β€’user-specific data should not be shared via public caches
β€’use private and often revalidation or even no-store for very sensitive state
β€’separate cacheable shell/assets from private API data

Scenario 4: News homepage HTML

Question: Should HTML be long-cached?

Strong answer:

β€’usually not with long immutable TTL
β€’prefer revalidation or short freshness
β€’HTML changes more frequently and controls references to current assets/content

Scenario 5: Slow repeat loads despite caching

Question: Why might that happen?

Strong answer:

β€’browser may be revalidating on each request
β€’no-cache may force network checks
β€’resources may lack explicit freshness lifetime
β€’service worker or fetch cache mode may affect behavior
β€’CDN/browser policy mismatch can reduce hit rate

Scenario 6: Wrong language served from cache

Question: What likely happened?

Strong answer:

β€’representation varied by language but cache key was incomplete
β€’missing or incorrect Vary: Accept-Language

Scenario 7: Personalized content leaked via CDN

Question: What likely went wrong?

Strong answer:

β€’response was shared-cacheable when it should have been private
β€’cache key or Vary policy did not safely separate representations

Best 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.

Key Takeaways

1HTTP caching is about deciding whether a response can be reused, for how long, and under what validation rules.
2Fresh cache hits are best because they avoid network requests entirely.
3ETag and Last-Modified are validators used for conditional requests after a cached response becomes stale.
4304 Not Modified avoids retransmitting the body, but still costs a network round trip.
5no-cache means storeable but must revalidate before reuse; no-store means do not store at all.
6immutable is ideal for content-hashed static assets that will not change at the same URL.
7s-maxage lets shared caches like CDNs have a different freshness policy from browsers.
8Vary helps caches choose the correct representation and prevents wrong-content reuse.
9stale-while-revalidate improves perceived speed by serving stale content while refreshing in the background.
10Versioned static assets should usually use long-lived immutable caching with hashed filenames.
11HTML documents often use short freshness or mandatory revalidation because they coordinate current content and asset references.
12Shared caches and browser caches are not the same thing; private user data needs extra care.
13Strong interview answers choose different caching policies for different resource types instead of one global rule.