Sign in
TodayMapLearnPracticeReview
Library
14 MINcoreCachingNot started

Client Caching

Client caching stores responses on the client — browser, mobile app, or desktop — so that subsequent reads are served locally without touching the network at all. It is the cheapest, fastest, and most distributed cache layer: zero server load, zero network latency, infinite horizontal scale (every client is its own cache). The cost is invalidation: once a response is on a client, the server cannot easily retract or update it, so client caches must be designed around TTLs, versioned URLs, and explicit cache-busting strategies.

Why this matters

Client caching is the most under-appreciated cache layer in system design. A browser-cached static asset serves millions of users at zero marginal cost — there is no faster or cheaper caching strategy. But the same property that makes it powerful (the server does not participate in the read) makes it dangerous: a cached response cannot be recalled, and a buggy cache header can serve stale content for the full TTL. Every frontend engineer must understand HTTP cache headers, every mobile engineer must understand app-level cache invalidation, and every backend engineer must design APIs that are safe to cache on the client. Mistakes here cause "I see the old version" bug reports that are notoriously hard to debug.

Prerequisites
  • Caching Strategies
Related
  • CDN Caching
  • Web Server Caching
  • HTTP — HyperText Transfer Protocol
Used in

Foundational.

Lesson

How it works

Client caching stores responses on the client — the browser, mobile app, or desktop app — so that the next request for the same resource is served locally without touching the network. It is the outermost cache layer in the standard cache hierarchy:

code
client cache → CDN → web-server cache → application cache → database cache → database

Each layer is faster than the one to its right and harder to invalidate. Client caching is the fastest (no network at all) and the hardest to invalidate (the server has no control over the client's storage).

There are three flavors of client caching, each with its own semantics:

  1. HTTP cache (browser cache). Built into the browser. Controlled by HTTP headers: Cache-Control, ETag, Last-Modified, Expires. Stores responses keyed by URL. No code needed on the client — the browser does it automatically.
  2. Service Worker cache. Explicit programmatic control from JavaScript. The service worker intercepts fetch events and can serve from a Cache object, allowing offline-first behavior. Used by Progressive Web Apps.
  3. App-level cache. Mobile and desktop apps maintain their own cache — image caches (Glide, SDWebImage, Kingfisher), API response caches, asset caches. Controlled entirely by app code, with whatever invalidation strategy the app chooses.

HTTP cache headers — the foundation of browser caching.

The browser's cache is controlled entirely by HTTP response headers. If you control the headers, you control the cache; if you don't set headers, the browser guesses (often conservatively, re-fetching when it doesn't have to).

The key headers:

  • Cache-Control: max-age=N — the response is fresh for N seconds. Within N seconds, the browser serves from cache with no network call at all. This is the strongest cache directive.
  • Cache-Control: public vs private — public means any cache (browser, CDN, proxy) may store it; private means only the end-user's browser may store it (e.g., responses containing user-specific data).
  • Cache-Control: no-cache — the browser must revalidate before using the cached response (conditional request with If-None-Match). The cached response is still stored, but it is always checked.
  • Cache-Control: no-store — the browser must NOT store the response at all. Used for sensitive data (bank balances, one-time tokens).
  • ETag + If-None-Match — server provides an opaque version tag. On subsequent requests the browser sends If-None-Match: <etag>. If unchanged, the server returns 304 Not Modified (no body) and the browser uses its cached copy.
  • Last-Modified + If-Modified-Since — same idea as ETag but time-based. ETag is more precise (handles sub-second changes), Last-Modified is simpler.
  • stale-while-revalidate=N — the response may be served stale for up to N seconds while the browser revalidates in the background. This is refresh-ahead at the client level.

The two-question decision: is this resource user-specific? → private. Is it the same for everyone? → public. Does it ever change? → no, set max-age=31536000 (one year) and version the URL. Does it change sometimes? → set a short max-age or use no-cache with ETag.

Cache-busting via versioned URLs

The cleanest way to make a client-cached resource updateable is to embed a version (or hash) in the URL: app.abc123.js instead of app.js. When the file changes, you change the URL to app.def456.js. The browser fetches the new URL because it has never seen it; the old URL remains in clients' caches until it expires (which is fine, because nothing references it anymore). This is why every modern bundler (webpack, Vite, esbuild) produces hashed filenames. Combine with Cache-Control: max-age=31536000, immutable for perfect caching: infinite cache lifetime, no revalidation traffic, instant updates when the URL changes.

App-level client caches.

Mobile and desktop apps do not have an HTTP cache that works transparently for API responses and images. They build their own caches with explicit invalidation strategies:

  • Image caches (Glide on Android, SDWebImage on iOS, Kingfisher on Swift): cache decoded images in memory and on disk, keyed by URL. Invalidation is TTL-based or by URL change (cache-busting).
  • API response caches: the app caches API responses for a TTL and revalidates. Often paired with conditional requests (If-None-Match) so the server can return 304 cheaply.
  • Offline-first caches: the app stores a full local copy of the user's data (often in SQLite) and syncs with the server. Reads are always local; writes are queued and synced when online. Used by Notion, Linear, GitHub mobile, and most modern apps.

The crucial difference from browser caching: the app controls invalidation, not the server. If the app caches a response with no expiry, the server cannot force it to refresh — the app will serve stale data forever. This is why mobile apps ship with cache TTLs baked in, and why server-side cache-busting (versioned URLs, cache-version query params) is essential.

The other major risk is disk space. Unlike a browser, which manages its own cache eviction, an app's cache lives in the user's storage and can grow unboundedly. Apps must implement LRU eviction and respect system storage pressure events or risk being uninstalled for being a storage hog.

When to use client caching:

  • Static assets (JS, CSS, images, fonts). Always. Versioned URLs + max-age=31536000, immutable.
  • Public, slowly-changing data (product catalog, pricing). Short TTL or stale-while-revalidate.
  • User-specific data with TTL tolerance (timeline, notifications). private cache with short TTL.
  • Offline-first apps (note-taking, email clients). Full local cache with sync.
  • Read-heavy APIs where the client can tolerate brief staleness.

When NOT to use client caching:

  • Sensitive data (bank balances, one-time tokens, PII). no-store — do not let it sit on the client.
  • Data that must be fresh on every read (real-time stock prices, live scores). no-cache with ETag, or no cache at all.
  • Data the server cannot invalidate (long TTLs without versioning). A 1-year TTL on a non-versioned URL means a buggy response is cached for a year.
  • Highly personalized data (per-user recommendations) without per-user cache keys. Caching by URL alone would leak one user's data to another.
Check yourself
core

You serve a JS bundle at `/app.js` with `Cache-Control: max-age=31536000`. You deploy a bug fix and rename nothing. What happens?

Pick one answer.

Check yourself
solid

An API response contains the logged-in user's bank balance. Which `Cache-Control` directive should you set?

Pick one answer.

Check yourself
interview

A mobile app caches API responses indefinitely with no TTL, and the backend changed the response format. After the update, the app crashes for some users. Why, and what is the fix?

Pick one answer.

Engineering mental model

Mental model. Think of Client Caching as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does Client Caching mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”

Design lens

Before choosing Client Caching, name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.

Original NO CAP systems visual for Client Caching.
Image unavailable. Original NO CAP systems visual for Client Caching.
Client Caching: a compact system-thinking visual.— Original NO CAP visual.
// Pseudocode
request = receive()
result = client_caching(request)
return result

// Production questions:
// 1. What happens on timeout?
// 2. Can this operation be retried safely?
// 3. What is the bottleneck?
A minimal engineering sketch for reasoning about Client Caching.

Back-of-the-envelope reasoning

Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.

Interactive sandboxdeterministic

Interactive thought experiment: Client Caching

Change the variables below and predict what breaks first in Client Caching. The production lab can later reuse these same inputs.

System pressure6%
Try this

Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.

Hint

If you are stuck on Client Caching, start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.

Check yourself
solid

You increase traffic by 10× in a system using Client Caching. What should you inspect first?

Pick one answer.

Check yourself
interview

Which statement is the safest engineering habit when using Client Caching?

Pick one answer.

Try this
interview

You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.

Production scenario: your system uses Client Caching, traffic suddenly spikes, and p99 latency doubles. What is your first move?

Interview drill

Answer this without notes: When would you choose Client Caching, and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.

Engineering lens

For Client Caching, reason in this order: what data is hottest, where can it live closer to the caller, what makes it stale, and what happens on a miss or cache failure. A cache is an optimization boundary, not the source of truth.

Numerical sanity check

A useful first-order model is cache_load = request_rate × (1 - hit_rate). If traffic is 20,000 req/s and the hit rate is 90%, roughly 2,000 req/s still reaches the origin before considering misses caused by expiration or eviction.

Check yourself
interview

Do not optimize for a memorized definition. Reason from the workload and failure mode.

You have a read-heavy Client Caching path. Traffic doubles overnight. What metric would you inspect first, and what would convince you to add another cache layer?

Pick one answer.

Trade-offs

What you gain, what you pay

Pros
  • +Zero server load — the request never reaches the server.
  • +Zero network latency — served from local disk or memory.
  • +Infinite horizontal scale — every client is its own cache, no shared infrastructure.
  • +Free — the user pays for the storage, not the operator.
  • +Offline-capable — combined with a service worker or app cache, the app works without connectivity.
Cons
  • −Hardest to invalidate — the server cannot reach into the client to evict.
  • −Buggy responses are cached for the full TTL — no rollback.
  • −Disk space on the client is finite and shared with other apps — must implement LRU eviction.
  • −Per-user cache keys are required for personalized data — easy to leak across users.
  • −Debugging is hard — "works on my machine" because the cache state is invisible to the operator.
Failure modes

How this breaks in production

  • Long TTL on non-versioned URL → buggy response cached for months.
  • Per-user data cached under a shared URL → cross-user data leakage.
  • Sensitive data cached on a shared device → next user sees the previous user's data.
  • App cache with no TTL → stale response parsed by new code → crash.
  • Service worker stuck serving an old version → app cannot update until SW is forcibly updated.
  • Cache key collisions across API versions → mixed-format responses.
Common mistakes

Don't fall into these traps

  • •Setting `max-age=31536000` on a non-versioned URL — deploy-time bug fix is invisible for a year.
  • •Using `Cache-Control: public` for user-specific data — leaks across users via shared proxies.
  • •Forgetting `no-store` on sensitive responses — bank balances cached on shared library computers.
  • •Not implementing cache eviction in mobile apps — app storage grows unboundedly until uninstall.
  • •Treating the client cache as just another Redis — it is not observable, not invalidate-able, and shared with the user's other apps.
  • •Not versioning cache keys when changing API response formats — old cached responses parsed by new code → crashes.
Where you see it

Real systems using this

Every web browser — HTTP cache is built in.Every modern mobile app — image caches (Glide, SDWebImage, Kingfisher) and API response caches.Progressive Web Apps (Twitter Lite, Pinterest) — service worker caches for offline use.Offline-first desktop apps (Notion, Linear) — full local SQLite cache with sync.Service worker caches in modern web apps — explicit programmatic cache control.
Teardowns

How real systems implement this

  • Webpack / Vite / esbuild hashed bundle filenames — Modern JS bundlers emit content-hashed filenames (app.abc123.js) so that static assets can be cached for a year (`max-age=31536000, immutable`) and updates are deployed by changing the URL — the canonical client caching pattern.
  • Twitter Lite / Pinterest PWA — Service workers cache the app shell and static assets for offline-first use. The shell is versioned; updates ship as a new service worker version that takes over on next page load.
  • Glide (Android) / SDWebImage (iOS) image caches — Image-loading libraries cache decoded images in memory and on disk, keyed by URL. Invalidation is TTL-based or via URL change. LRU eviction keeps disk usage bounded.
  • Notion / Linear offline-first local cache — Desktop and mobile clients maintain a full SQLite replica of the user's data. Reads are always local (sub-ms); writes are queued and synced via a delta protocol when online.
Interview prompts

Practice saying it out loud

  • Q1How would you cache static assets for a website with millions of users? What headers would you set, and how would you handle updates?
  • Q2A user reports seeing stale data after a deploy. Walk through every cache layer where the stale data could be hiding.
  • Q3Your mobile app crashes for some users after a backend API format change. What went wrong, and how do you prevent it next time?
  • Q4When should you use `no-store` vs `no-cache` vs a short `max-age`?
  • Q5How do service workers change the client caching story? What new capabilities and risks do they introduce?
Research

Further reading & references

System Design Primer
Open source
ByteByteGo — Scale from zero to millions
ByteByteGo
System Design Tutorial
GeeksforGeeks
System Design Roadmap
roadmap.sh
Caching reference
Reference
Caching reference
Reference
Caching reference
Reference

Core explanations are original NO CAP material. External references are provided for deeper study and standards.

What next?

Mark as understood once the mental model clicks.

Next recommended

CDN Caching