HTTP CachingCache-ControlETagHeadersPerformance

HTTP Caching & Headers

Master the caching layer that makes the web fast. Understand how Cache-Control, ETag, and conditional requests let browsers skip network round trips entirely — and why getting caching wrong causes stale data bugs that are painful to debug.

28 min read14 sections
01

Overview

HTTP caching lets browsers store copies of server responses and reuse them for future requests — skipping DNS, TCP, TLS, and server processing entirely. A cached response loads in under 1ms. A network request takes 200ms+. Caching is the single biggest performance lever on the web.

Caching behavior is controlled by HTTP headers — primarily Cache-Control. The server tells the browser how long a response is valid, whether it can be stored, and how to check if it's still fresh. Get these headers right and your site loads instantly on repeat visits. Get them wrong and users see stale data or make unnecessary requests.

This topic connects directly to the HTTP lifecycle — caching short-circuits the entire flow. It's also one of the most practical interview topics: interviewers expect you to know the difference between no-cache and no-store, how ETags work, and how to design a caching strategy for different resource types.

Why this matters

The fastest HTTP request is the one that never happens. Proper caching eliminates 80-95% of network requests on repeat visits. For a site with 40 resources, that's 35+ requests that load from disk in <1ms instead of traveling across the internet.

02

The Problem: Repeated Requests

Without caching, every page load fetches every resource from the server — even if nothing has changed. The same CSS file, the same JavaScript bundle, the same logo image — downloaded again and again.

Without Caching — Page Reloadtext
User visits example.com/dashboard:

  GET /dashboard200 OK (14KB HTML)      ~200ms
  GET /styles.css200 OK (45KB CSS)       ~180ms
  GET /app.js200 OK (120KB JS)       ~220ms
  GET /logo.png200 OK (8KB image)      ~150ms
  GET /api/user200 OK (2KB JSON)       ~250ms
  GET /fonts/inter.woff2200 OK (35KB font)      ~190ms

  Total: 6 requests, 224KB transferred, ~250ms (parallel)

User reloads the page (nothing changed):

  GET /dashboard200 OK (14KB HTML)      ~200ms
  GET /styles.css200 OK (45KB CSS)       ~180ms
  GET /app.js200 OK (120KB JS)       ~220ms
  GET /logo.png200 OK (8KB image)      ~150ms
  GET /api/user200 OK (2KB JSON)       ~250ms
  GET /fonts/inter.woff2200 OK (35KB font)      ~190ms

  Same 6 requests, same 224KB. Nothing changed, but the
  browser downloaded everything again. Wasted bandwidth,
  wasted time, wasted server resources.
📡

Wasted Bandwidth

Users re-download the same files on every visit. On mobile networks, this burns through data plans and drains battery with unnecessary radio usage.

🐌

Slower Load Times

Every resource requires a full HTTP lifecycle — DNS, TCP, TLS, request, response. Even with HTTP/2, network latency adds 100-300ms per uncached resource.

🔥

Server Overload

Without caching, every page view hits the server for every resource. At scale, this means millions of redundant requests serving identical bytes.

The core insight

Most web resources don't change between page loads. Your CSS file is the same today as it was 5 minutes ago. Your logo hasn't changed in months. Caching tells the browser: "you already have this — use your copy."

03

What Is HTTP Caching?

HTTP caching is a mechanism where the browser (or an intermediary like a CDN) stores a copy of a server response and reuses it for subsequent requests to the same URL. The server controls caching behavior through response headers.

Where Caches Live

💻

Browser Cache (Private)

Stored on the user's device. Only serves that specific user. Controlled by Cache-Control: private. Stores HTML, CSS, JS, images, API responses.

🌐

CDN Cache (Shared)

Stored on edge servers worldwide. Serves all users in a region. Controlled by Cache-Control: public. Ideal for static assets that are the same for everyone.

🔀

Proxy Cache (Shared)

Corporate proxies, ISP caches, or reverse proxies (Nginx, Varnish). Sit between browser and origin server. Respect Cache-Control: public/private directives.

Cache Lookup Flowtext

Browser requests GET /styles.css:

  ┌─────────────────┐
Browser CacheIs there a cached copy?
  │  (private)      │  Is it still fresh (not expired)?
  └────────┬────────┘

     HIT & FRESH ──→ Use cached copy. Done. (<1ms)

     MISS or STALE

  ┌─────────────────┐
CDN / ProxyIs there a shared cached copy?
  │  (shared)       │  Is it still fresh?
  └────────┬────────┘

     HIT & FRESH ──→ Return cached copy. (~5-20ms)

     MISS or STALE

  ┌─────────────────┐
Origin ServerGenerate fresh response.
  │                 │  Set caching headers.
  └─────────────────┘  Return to browser. (~100-500ms)

Each level checks: do I have it? Is it still valid?
The closer the cache hit, the faster the response.

Fresh vs stale

A cached response is fresh if it hasn't exceeded its max-age. A stale response has expired but may still be valid — the browser can ask the server to validate it (conditional request) instead of downloading the full resource again.

04

Cache-Control Header

Cache-Control is the primary header that controls caching behavior. It's set by the server in the response and tells the browser (and intermediaries) exactly how to cache the response.

Key Directives

DirectiveMeaningExample
max-age=NCache is fresh for N seconds. No network request needed during this window.Cache-Control: max-age=3600 (1 hour)
no-cacheCache the response, but ALWAYS revalidate with the server before using it. Does NOT mean 'don't cache'.Cache-Control: no-cache
no-storeDo NOT cache at all. Every request goes to the server. Used for sensitive data.Cache-Control: no-store
publicAny cache (browser, CDN, proxy) can store this response. Used for shared resources.Cache-Control: public, max-age=86400
privateOnly the browser can cache this. CDNs and proxies must NOT store it. Used for user-specific data.Cache-Control: private, max-age=600
immutableThe resource will NEVER change. Browser skips revalidation even on hard reload.Cache-Control: public, max-age=31536000, immutable
s-maxage=NLike max-age but only for shared caches (CDN/proxy). Overrides max-age for intermediaries.Cache-Control: public, max-age=60, s-maxage=3600
stale-while-revalidate=NServe stale content for N seconds while fetching a fresh copy in the background.Cache-Control: max-age=60, stale-while-revalidate=300

no-cache vs no-store — The Most Confused Pair

no-cache vs no-storetext
Cache-Control: no-cache
─────────────────────────────────────────────
Browser DOES cache the response
But MUST revalidate with server before using it
Browser sends conditional request (If-None-Match / If-Modified-Since)
Server responds 304 (use cache) or 200 (new data)
Saves bandwidth if resource hasn't changed (no body in 304)

  Use for: HTML pages, API responses that change frequently
  Cost: 1 round trip per request (but no download if unchanged)


Cache-Control: no-store
─────────────────────────────────────────────
Browser does NOT cache the response at all
Every request downloads the full resource
No conditional requests, no 304s
Full download every time

  Use for: Sensitive data (bank statements, medical records)
  Cost: Full download on every request

  ⚠️ "no-cache" does NOT mean "don't cache"
     "no-store" means "don't cache"

The interview trap

"What's the difference between no-cache and no-store?" is asked in almost every frontend interview that covers caching. no-cache = cache it but always check with the server first. no-store = don't cache it at all. The name "no-cache" is misleading — it should have been called "must-revalidate."

05

Expiration vs Validation

HTTP caching uses two complementary strategies to decide whether a cached response can be reused. Understanding both is essential for designing effective caching policies.

Strategy 1: Expiration (Time-Based)

The server says "this response is valid for X seconds." During that window, the browser uses the cached copy without contacting the server at all. Zero network requests. Maximum speed.

Expiration Flowtext
Server response:
  Cache-Control: max-age=3600  (fresh for 1 hour)

Request at T+0:    → Network request200 OKcached
Request at T+30m:  → Cache HIT (still fresh) → no network request (<1ms)
Request at T+59m:  → Cache HIT (still fresh) → no network request (<1ms)
Request at T+61m:  → Cache STALE (expired) → needs revalidation or fresh fetch

Strategy 2: Validation (Revalidation)

When a cached response expires (or uses no-cache), the browser asks the server: "has this changed?" If not, the server responds with 304 Not Modified and no body — saving bandwidth.

Validation Flowtext
Server response (first request):
  HTTP/2 200 OK
  Cache-Control: no-cache
  ETag: "abc123"
  Content-Length: 45000
  [45KB body]

Browser request (subsequent):
  GET /styles.css
  If-None-Match: "abc123""I have version abc123, is it still good?"

Server response (resource unchanged):
  HTTP/2 304 Not Modified"Yes, your copy is still good"
  ETag: "abc123"
  Content-Length: 0No body! Saves 45KB of bandwidth

Server response (resource changed):
  HTTP/2 200 OK"No, here's the new version"
  ETag: "def456"
  Content-Length: 47000
  [47KB body]
ExpirationValidation
How it worksBrowser uses cached copy without asking serverBrowser asks server if cached copy is still valid
Network request?None (during fresh period)Yes, but response may be tiny (304, no body)
Speed<1ms (instant, from disk)~50-200ms (1 round trip, but no download if unchanged)
Freshness guaranteeMay serve stale data until max-age expiresAlways up-to-date (server confirms on every request)
Best forStatic assets that rarely change (JS, CSS, images with hashes)Dynamic content that changes unpredictably (HTML, API responses)
HeadersCache-Control: max-age=NETag + If-None-Match, or Last-Modified + If-Modified-Since

Best practice: combine both

Use expiration for the fast path (serve from cache instantly) and validation as a fallback (check with server when expired). Example: Cache-Control: max-age=3600 + ETag: "abc123". Fresh for 1 hour, then revalidates.

06

Conditional Requests

Conditional requests are how validation works in practice. The browser sends a request with a condition: "give me this resource only IF it has changed." If it hasn't changed, the server responds with 304 and no body.

ETag + If-None-Match

An ETag is a version identifier for a resource — typically a hash of the content. It's the most reliable validation mechanism because it detects any change to the content, no matter how small.

1

Server sends ETag with the response

First response includes ETag: "abc123" — a fingerprint of the current content. The browser stores this alongside the cached response.

2

Browser sends If-None-Match on next request

When the cache expires, the browser sends If-None-Match: "abc123" — asking "is my version still current?"

3

Server compares ETags

If the current ETag matches → 304 Not Modified (no body, use cache). If different → 200 OK with new content and new ETag.

ETag Conditional Requesttext
── First Request ──────────────────────────────────

  GET /api/products HTTP/2
  Host: api.example.com

  HTTP/2 200 OK
  Cache-Control: max-age=60
  ETag: "a1b2c3d4"
  Content-Type: application/json
  Content-Length: 15000

  [15KB JSON body]

── After 60s (cache expired) ──────────────────────

  GET /api/products HTTP/2
  If-None-Match: "a1b2c3d4""I have this version"

  HTTP/2 304 Not Modified"Your version is still good"
  ETag: "a1b2c3d4"
  Content-Length: 0No body! Saved 15KB

  Browser uses cached copy. Total transfer: ~200 bytes (headers only).

Last-Modified + If-Modified-Since

An older mechanism that uses timestamps instead of content hashes. Less precise than ETags (1-second resolution) but simpler to implement.

Last-Modified Conditional Requesttext
── First Request ──────────────────────────────────

  HTTP/2 200 OK
  Last-Modified: Sat, 21 Mar 2026 14:30:00 GMT
  Content-Length: 45000

  [45KB body]

── Subsequent Request ─────────────────────────────

  GET /styles.css HTTP/2
  If-Modified-Since: Sat, 21 Mar 2026 14:30:00 GMT

  HTTP/2 304 Not ModifiedNot changed since that date
  Content-Length: 0
ETagLast-Modified
MechanismContent hash / version stringTimestamp of last change
PrecisionByte-level (any change detected)1-second resolution
Request headerIf-None-MatchIf-Modified-Since
ReliabilityHigh — detects all changesLower — misses sub-second changes
Use caseAPI responses, dynamic contentStatic files, simple setups

Prefer ETags

ETags are more reliable than Last-Modified because they detect any content change, not just timestamp changes. A file can be modified twice in the same second (Last-Modified misses this) or touched without changing content (Last-Modified triggers a false invalidation). Use ETags as your primary validation mechanism.

07

Other Important Headers

Beyond Cache-Control and ETags, several other headers influence caching behavior. Some are legacy but still appear in production systems.

HeaderPurposeNotes
ExpiresSets an absolute expiration date for the cached responseLegacy. Superseded by Cache-Control: max-age. If both are present, max-age wins. Uses absolute dates which break if clocks are wrong.
VaryTells caches that the response varies based on specific request headersVary: Accept-Encoding means gzip and non-gzip responses are cached separately. Vary: Cookie means each user gets a separate cache entry. Critical for CDNs.
Pragma: no-cacheHTTP/1.0 equivalent of Cache-Control: no-cacheLegacy. Only included for backward compatibility with very old proxies. Always use Cache-Control instead.
AgeHow long the response has been in a shared cache (seconds)Set by CDNs/proxies. If Age: 300 and max-age: 3600, the response has 3300 seconds of freshness remaining.
Clear-Site-DataInstructs the browser to clear cached data for the originClear-Site-Data: "cache" clears all cached responses. Useful for logout flows to ensure no sensitive data remains cached.

The Vary Header — Why It Matters for CDNs

Vary Header Exampletext
Server response:
  Cache-Control: public, max-age=3600
  Vary: Accept-Encoding
  Content-Encoding: gzip

What this means for the CDN:
  The response varies based on Accept-Encoding.
  A client sending "Accept-Encoding: gzip" gets one cached version.
  A client sending "Accept-Encoding: br" gets a different cached version.
  A client sending no Accept-Encoding gets yet another version.

  Without Vary, the CDN might serve a gzip response to a client
  that doesn't support gzip — breaking the page.

Common Vary values:
  Vary: Accept-EncodingDifferent compression formats
  Vary: Accept-LanguageDifferent language versions
  Vary: CookieDifferent per user (⚠️ defeats CDN caching)
  Vary: *                   → Never cache (equivalent to no-store for shared caches)

Watch out for Vary: Cookie

Setting Vary: Cookie on a CDN-cached response means every unique cookie value creates a separate cache entry. Since cookies are often unique per user, this effectively disables CDN caching. Use Cache-Control: private instead for user-specific responses.

08

Caching Strategies

Different resources need different caching strategies. The key is matching the cache policy to how often the resource changes and how critical freshness is.

Strategy 1: Cache Forever (Fingerprinted Static Assets)

Immutable Static Assetstext
Files with content hashes in the filename:
  app.a1b2c3.js
  styles.d4e5f6.css
  logo.g7h8i9.png

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

Why this works:
The filename changes when the content changes (cache busting)
So the URL is effectively a unique version identifier
Safe to cache for 1 year (31536000 seconds)
"immutable" tells the browser: don't even revalidate on hard reload
CDNs cache it globallyserved from edge nodes worldwide

When the file changes:
Build tool generates a new hash: app.x9y8z7.js
HTML references the new filename
Browser sees a new URLfetches it fresh
Old cached version is never requested again

Strategy 2: Always Revalidate (HTML Pages)

HTML — Always Freshtext
Cache-Control: no-cache
ETag: "page-v42"

Why:
HTML is the entry pointit references CSS/JS filenames
If HTML is stale, it may reference old CSS/JS files that no longer exist
no-cache ensures the browser always checks with the server
ETag enables 304 responsesif HTML hasn't changed, no download needed
Cost: 1 round trip per page load, but usually just headers (304)

Strategy 3: Short Cache (API Responses)

API Responsestext
Cache-Control: private, max-age=60, stale-while-revalidate=300

Why:
"private"user-specific data, don't cache on CDN
"max-age=60"fresh for 1 minute (acceptable staleness)
"stale-while-revalidate=300"if stale, serve cached version
    immediately while fetching fresh data in the background
Good for dashboards, feeds, user profiles
Balances freshness with performance

Strategy 4: Never Cache (Sensitive Data)

Sensitive Datatext
Cache-Control: no-store

Why:
Bank account balances, medical records, payment details
Must NEVER be stored on disk (even encrypted)
Every request fetches fresh from the server
Most restrictive policyuse only when necessary
Resource TypeStrategyCache-ControlValidation
Fingerprinted JS/CSS/imagesCache foreverpublic, max-age=31536000, immutableNot needed (URL changes)
HTML pagesAlways revalidateno-cacheETag
API responses (public)Short cache + CDNpublic, max-age=60, s-maxage=300ETag
API responses (private)Short cache + SWRprivate, max-age=60, stale-while-revalidate=300ETag
Sensitive dataNever cacheno-storeNone
FontsLong cachepublic, max-age=31536000Not needed (rarely change)

The golden pattern

Fingerprint your static assets (content hash in filename) + cache them forever + always revalidate your HTML. This gives you instant loads for returning visitors AND instant cache invalidation when you deploy — because the HTML points to new filenames.

09

Real-World Example

Let's see the difference caching makes for a real e-commerce product page with typical resources.

❌ Without Caching Headers

First Visit (no caching)text
GET /products/shoes200 OK  (12KB HTML)     ~200ms
GET /app.js200 OK  (150KB JS)      ~250ms
GET /styles.css200 OK  (40KB CSS)      ~180ms
GET /logo.png200 OK  (15KB image)    ~160ms
GET /hero.jpg200 OK  (80KB image)    ~220ms
GET /api/products/123200 OK  (3KB JSON)      ~300ms
GET /fonts/inter.woff2200 OK  (35KB font)     ~190ms

Total: 335KB, 7 requests, ~300ms (parallel with HTTP/2)

── User navigates to another product, then comes back ──

GET /products/shoes200 OK  (12KB HTML)     ~200ms
GET /app.js200 OK  (150KB JS)      ~250msSAME FILE!
GET /styles.css200 OK  (40KB CSS)      ~180msSAME FILE!
GET /logo.png200 OK  (15KB image)    ~160msSAME FILE!
GET /hero.jpg200 OK  (80KB image)    ~220msSAME FILE!
GET /api/products/123200 OK  (3KB JSON)      ~300ms
GET /fonts/inter.woff2200 OK  (35KB font)     ~190msSAME FILE!

Total: 335KB again. 100% wasted bandwidth for unchanged files.

✅ With Proper Caching Headers

Return Visit (with caching)text
── Server response headers ──

/products/shoesCache-Control: no-cache, ETag: "page-v5"
/app.a1b2c3.jsCache-Control: public, max-age=31536000, immutable
/styles.d4e5f6.cssCache-Control: public, max-age=31536000, immutable
/logo.pngCache-Control: public, max-age=86400
/hero.jpgCache-Control: public, max-age=86400
/api/products/123Cache-Control: private, max-age=60, ETag: "prod-v8"
/fonts/inter.woff2Cache-Control: public, max-age=31536000

── Return visit (within 60 seconds) ──

GET /products/shoes304 Not Modified (0KB)   ~80msjust headers
GET /app.a1b2c3.js     → (from cache)             <1msno request!
GET /styles.d4e5f6.css → (from cache)             <1msno request!
GET /logo.png          → (from cache)             <1msno request!
GET /hero.jpg          → (from cache)             <1msno request!
GET /api/products/123  → (from cache)             <1msstill fresh
GET /fonts/inter.woff2 → (from cache)             <1msno request!

Total: ~0.5KB transferred, 1 network request, ~80ms
Savings: 99.8% less bandwidth, 73% faster
MetricWithout CachingWith Caching
Network requests7 requests1 request (304)
Data transferred335KB~0.5KB (headers only)
Load time~300ms~80ms
Server load7 requests processed1 lightweight 304

The compound effect

This is one page load. Multiply by thousands of users and dozens of page navigations per session. Proper caching can reduce your server bandwidth by 90%+ and your CDN costs proportionally. It's one of the highest-ROI optimizations in web development.

10

Performance Insights

Caching is the highest-leverage performance optimization on the web. Here are the specific techniques and their impact.

✓ Done

Fingerprint Static Assets + Cache Forever

Content-hash filenames (app.a1b2c3.js) with max-age=31536000 and immutable. Eliminates all network requests for static assets on repeat visits. The single most impactful caching strategy.

✓ Done

Always Revalidate HTML with ETags

Cache-Control: no-cache + ETag on HTML pages. Ensures users always get the latest HTML (which references the latest asset filenames) while saving bandwidth with 304 responses when unchanged.

✓ Done

stale-while-revalidate for API Responses

Serves cached API data instantly while fetching fresh data in the background. Users see content immediately (even if slightly stale) and get updated data on the next interaction. Great for dashboards and feeds.

✓ Done

CDN Caching with s-maxage

Use s-maxage to set longer cache durations on CDN edges than in the browser. Example: max-age=60, s-maxage=3600 — browser revalidates after 1 minute, CDN caches for 1 hour. Reduces origin server load dramatically.

→ Could add

Preload Critical Resources

Use <link rel='preload'> for fonts and critical CSS that the browser discovers late in the HTML. Combined with long cache durations, this ensures critical resources are both cached and loaded early.

→ Could add

Service Worker Cache (Offline Support)

A Service Worker can intercept requests and serve from a programmatic cache — enabling offline support and custom caching strategies beyond what HTTP headers offer. Useful for PWAs.

Measure your cache hit rate

In Chrome DevTools Network tab, look at the "Size" column. Responses served from cache show "(disk cache)" or "(memory cache)" instead of a byte count. A well-cached site should show 80-95% of requests served from cache on repeat visits.

11

Common Mistakes

🚫

Using no-store when you mean no-cache

no-store prevents ALL caching — every request downloads the full resource. Developers often use it when they just want fresh data, but no-cache (revalidate every time) would save bandwidth with 304 responses.

Use no-cache + ETag for content that must be fresh. Reserve no-store only for truly sensitive data (bank balances, medical records) that must never be stored on disk.

📛

Long max-age on non-fingerprinted assets

Setting max-age=31536000 on /app.js (no hash in filename) means users are stuck with the cached version for a year. When you deploy a fix, users won't see it until the cache expires.

Either fingerprint your assets (app.a1b2c3.js) so the URL changes on deploy, or use short max-age with ETag validation. Never set long max-age on URLs that don't change when content changes.

🔄

Forgetting to cache static assets at all

Without Cache-Control headers, browsers use heuristic caching (based on Last-Modified date) which is unpredictable. Some resources get cached, some don't, and the duration varies.

Explicitly set Cache-Control on every response. Don't rely on browser heuristics. Be intentional: cache forever (fingerprinted), revalidate always (HTML), or never cache (sensitive).

🍪

Caching user-specific responses on CDN

Setting Cache-Control: public on responses that contain user-specific data (account info, personalized content) means the CDN serves User A's data to User B.

Use Cache-Control: private for any response that varies per user. This restricts caching to the browser only. For CDN-cached pages with some personalization, load user-specific data via separate API calls.

Not setting Vary: Accept-Encoding

If your server sends gzip-compressed responses without Vary: Accept-Encoding, a CDN might cache the gzip version and serve it to a client that doesn't support gzip — breaking the page.

Always include Vary: Accept-Encoding when your server supports content negotiation for compression. Most CDNs and web servers handle this automatically, but verify it.

12

Interview Questions

Q:What is the difference between no-cache and no-store?

A: no-cache stores the response in cache but requires revalidation with the server before every use — the browser sends a conditional request (If-None-Match) and the server responds with 304 (use cache) or 200 (new data). no-store prevents caching entirely — every request downloads the full resource. Use no-cache for content that must be fresh (HTML pages), no-store for sensitive data that must never be stored on disk.

Q:What is an ETag and how does it work?

A: An ETag is a version identifier for a resource, typically a hash of the content (e.g., ETag: "abc123"). When the browser has a cached response with an ETag, it sends If-None-Match: "abc123" on subsequent requests. The server compares ETags — if they match, it responds with 304 Not Modified (no body), saving bandwidth. If different, it sends 200 with the new content and a new ETag.

Q:How would you design a caching strategy for a web application?

A: Three-tier approach: (1) Fingerprinted static assets (JS, CSS, images with content hashes) get Cache-Control: public, max-age=31536000, immutable — cached forever, URL changes on deploy. (2) HTML pages get Cache-Control: no-cache with ETag — always revalidated but 304 saves bandwidth. (3) API responses get Cache-Control: private, max-age=60 with stale-while-revalidate — short freshness window with instant stale serving.

Q:What is a 304 Not Modified response?

A: 304 is a status code that tells the browser 'your cached copy is still valid — use it.' It's sent in response to a conditional request (If-None-Match or If-Modified-Since). The 304 response has no body, only headers, saving the bandwidth of re-downloading the resource. It's the mechanism behind cache validation — the browser checks with the server, and the server confirms the cache is still fresh.

Q:What is cache busting and why is it needed?

A: Cache busting is the technique of changing a resource's URL when its content changes, forcing browsers to fetch the new version instead of using the cached old one. The most common approach is content-hash filenames (app.a1b2c3.js). When you deploy new code, the build tool generates a new hash (app.x9y8z7.js), the HTML references the new URL, and browsers fetch it fresh. This lets you set very long max-age on static assets without worrying about stale caches.

Q:What does the Vary header do?

A: Vary tells caches that the response depends on specific request headers. Vary: Accept-Encoding means the cache should store separate versions for gzip, brotli, and uncompressed responses. Without Vary, a CDN might serve a gzip response to a client that doesn't support it. Vary: Cookie effectively disables shared caching because each user has different cookies. Vary: * means never cache in shared caches.

Q:What is stale-while-revalidate?

A: stale-while-revalidate is a Cache-Control directive that lets the browser serve a stale cached response immediately while fetching a fresh copy in the background. Example: max-age=60, stale-while-revalidate=300 means the response is fresh for 60 seconds, then for the next 300 seconds the browser serves the stale version instantly and updates the cache in the background. The user gets instant response times with eventual freshness.

Q:Why should you use Cache-Control: private for user-specific data?

A: Cache-Control: private restricts caching to the browser only — CDNs and shared proxies must not store the response. Without it (or with public), a CDN could cache User A's personalized dashboard and serve it to User B. This is a security and correctness issue. Use private for any response containing user-specific data: account info, personalized feeds, authenticated API responses.

13

Practice Section

1

The Stale Dashboard

Users report that after a deploy, they still see the old version of your dashboard for hours. Your JS bundle is served as /app.js with Cache-Control: public, max-age=86400. What's wrong and how do you fix it?

Answer: The filename doesn't change on deploy, so browsers serve the cached /app.js for 24 hours. Fix: add content hashes to filenames (app.a1b2c3.js) so the URL changes on every deploy. Set max-age=31536000 with immutable on hashed files. Always revalidate the HTML (no-cache + ETag) so it picks up new asset filenames immediately.

2

The Stale API Data

Your dashboard API returns user-specific data with Cache-Control: public, max-age=3600. A user logs out and another user logs in on the same shared computer. The second user sees the first user's data. What happened?

Answer: Cache-Control: public allows shared caches (and the browser cache keyed by URL) to store the response. The second user's request hits the same cached response. Fix: use Cache-Control: private (browser-only cache) and add Vary: Cookie or Vary: Authorization so different users get different cache entries. For sensitive data, consider no-store.

3

The Bandwidth Bill

Your CDN bandwidth bill is unexpectedly high. Investigation shows that your 150KB JavaScript bundle is being downloaded on every page load despite having Cache-Control: max-age=3600. The bundle hasn't changed in weeks. What could cause this?

Answer: Possible causes: (1) The HTML page has no-store, preventing the browser from caching the HTML that references the JS — each visit generates a new request. (2) The server isn't sending ETag or Last-Modified, so the browser can't do conditional requests. (3) Vary: Cookie is set, creating a separate cache entry per user. Fix: fingerprint the JS file, set immutable, and ensure the HTML uses no-cache (not no-store).

4

The Cache Invalidation Problem

You need to immediately invalidate a cached API response across all users. The response was served with Cache-Control: public, max-age=3600 and is cached on your CDN. How do you force all users to get fresh data?

Answer: For the CDN: use the CDN's purge/invalidation API to clear the cached response from all edge nodes. For browser caches: you can't force invalidation — the browser will serve the cached version until max-age expires. For future prevention: use shorter max-age with stale-while-revalidate, or use no-cache with ETag so every request validates with the server.

5

The Font Loading Delay

Your custom font (200KB) is re-downloaded on every page navigation within your SPA, causing a flash of unstyled text each time. The font file never changes. How would you optimize this?

Answer: Set Cache-Control: public, max-age=31536000 on the font file — it never changes, so cache it forever. Add <link rel='preload' as='font' crossorigin> in the HTML to start downloading it early. The combination of long cache + preload means the font loads instantly from cache on subsequent navigations with no flash.

14

Cheat Sheet

Quick Revision Cheat Sheet

Cache-Control: max-age=N: Response is fresh for N seconds. No network request during this window. Browser serves from cache in <1ms.

Cache-Control: no-cache: Cache the response but ALWAYS revalidate with the server before using it. Does NOT mean 'don't cache'. Enables 304 responses.

Cache-Control: no-store: Do NOT cache at all. Every request downloads the full resource. Use only for sensitive data (bank statements, medical records).

public vs private: public: any cache (browser, CDN, proxy) can store it. private: only the browser can store it. Use private for user-specific data to prevent CDN leaks.

ETag + If-None-Match: ETag is a content fingerprint. Browser sends If-None-Match with the cached ETag. Server responds 304 (use cache) or 200 (new data). Saves bandwidth.

304 Not Modified: Server confirms the cached copy is still valid. No body in the response — just headers. The browser uses its cached version. Costs 1 round trip but saves the download.

Fingerprinted assets: Content hash in filename (app.a1b2c3.js). Cache forever with max-age=31536000, immutable. URL changes when content changes — automatic cache busting.

HTML caching: Always use no-cache + ETag for HTML. HTML references asset filenames — if HTML is stale, it may point to old assets. Revalidation ensures users get the latest entry point.

stale-while-revalidate: Serve stale content instantly while fetching fresh data in the background. Great for API responses where slight staleness is acceptable. Instant UX with eventual freshness.

s-maxage: Cache duration for shared caches (CDN/proxy) only. Overrides max-age for intermediaries. Use to cache longer on CDN than in browser: max-age=60, s-maxage=3600.

Vary header: Tells caches the response varies by request header. Vary: Accept-Encoding = separate cache per compression. Vary: Cookie = separate cache per user (defeats CDN caching).

The golden rule: The fastest request is no request. Fingerprint + cache forever for static assets. Revalidate HTML. Short cache + SWR for APIs. no-store only for sensitive data.