Caching Strategies

Browser Caching

0 of 8 complete

0%

Contents

Back|Caching StrategiesBrowser Caching
1/8
21 min left
  1. Home
  2. System Design: Foundation
  3. Caching Strategies
  4. Browser Caching
Prerequisites
Application Cacherequired
Related Topics
Lazy LoadingWeb & Content DeliveryCachingCore FundamentalsContent Delivery Network (CDN)Load Balancing & ProxiesData Locality OptimizationData Replication & DistributionIn-Memory DatabasesDatabase Types & Storage
Previous lessonApplication CacheNext lessonClient-Side Caching

System Design

1 of 8
  • Foundation
  • Intermediate
  • Advanced
  • Capstone
  • AI Engineering

    • Foundation
    • Data, RAG and Agents
    • Evaluation, LLM Ops and Security

    systemdesign.academy

    • Home
    • Glossary
    • Interview prep
    • Reviews
    • About
    • Privacy
    • Terms

    this free lesson is part of

    System Design Masterclass

    • 770 lessons, beginner to principal
    • 90 real interview systems taken apart step by step
    • An AI tutor beside every lesson that answers and draws
    • Lifetime access, every new lesson included

    $49 once, by PayPal

    See the System Design course

    Save your progress and get the interview checklist PDF

    System Design Interview Cheat Sheet: the 7 interview stages, the 10 points a scorecard grades, 12 classic systems. 18 pages. Free with an account.

    you just read a free lesson from

    System Design Masterclass

    • 770 lessons, beginner to principal
    • 90 real interview systems taken apart step by step
    • An AI tutor beside every lesson that answers and draws
    • Lifetime access, every new lesson included

    $49 once, by PayPal

    See the System Design course

    The Cache You Cannot Reach

    Every cache so far in this chapter has had an off switch. You can flush Redis, purge a , restart a process. The browser cache has none of that, because it is on somebody else's laptop.

    Two panels. The left, caches you operate, shows your Redis and your CDN feeding a bar reading you can purge, ending in wrong for seconds. Its key reads: you can inspect yes, you can purge yes, a mistake lasts until you fix it. The right, the browser cache, shows a laptop and a million of them feeding a bar reading no way in, ending in wrong until it expires. Its key reads: you can inspect no, you can purge no, a mistake lasts the full max-age.
    ComparisonFigure 1. Every other cache in this chapter can be emptied

    There is no request you can send to a browser to make it forget something, on any browser, by any means. So a wrong Cache-Control header is not an incident you fix. It is one you wait out.

    That asymmetry shapes every recommendation that follows.

    after “The Cache You Cannot Reach”

    Want the reasoning behind this, step by step?

    The lesson that covers it: Redis Cache, in the System Design course.

    The AI tutor beside the lesson answers your follow-up questions, and can draw.

    Look inside the System Design course

    Three Outcomes, and the Best One Is Silent

    A sequence diagram with two lifelines, the browser and the server. Case one: nothing cached, a GET and the whole file comes back. Case two: cached but stale, a GET with If-None-Match, and the server returns 304 Not Modified with no body, noted as saved the bytes and paid the round trip. Case three: cached and fresh, no request at all, noted as there is no arrow here and that is the point.
    SequenceFigure 2. The best outcome draws no arrow, because nothing goes over the network

    A fresh cache entry under max-age is not a fast request. It is the absence of a request, which is a different category of thing: zero milliseconds, zero bytes, and it works on a train with no signal.

    Most advice quietly optimises for the middle case when the third one was available. It is worth measuring what that costs.

    What a 304 Actually Saves

    The script is at scripts/labs/browsercache/revalidate.py. It fetches real assets in full, then again with If-None-Match, and times both.

    A recorded terminal session. A table of assets with full 200 timings and 304 timings. Small json at 9,648 bytes took 124.5 ms in full and 120.2 ms as a 304, a 3.5 percent saving. Medium css at 232,803 bytes took 195.5 ms in full and 117.9 ms as a 304, a 39.7 percent saving. Small js from unpkg reports no ETag, so it cannot be revalidated at all.
    RecordedFigure 3. Both revalidations skipped every byte. Only one of them saved anything

    The 304 returns no body, so in both rows one hundred percent of the content bytes were avoided. On the small file that bought 3.5 percent of the time, which is nothing. On the large file it bought 39.7 percent, because there the bytes genuinely dominated.

    A bar chart in milliseconds, median of five requests. For the small json of 9,648 bytes, the full 200 bar is 124.5 and the 304 bar is 120.2. For the medium css of 232,803 bytes, the full 200 bar is 195.5 and the 304 bar is 117.9.
    ChartFigure 4. The saving tracks file size, and has nothing to do with freshness

    A 304 saves bytes. On a small asset the bytes were never the expensive part, so revalidating it costs almost exactly as much as sending it again. Revalidation is a optimisation being used as a freshness strategy.

    The Vocabulary, and the Trap in It

    Six directives. max-age=N means usable for N seconds without asking anybody. no-cache means store it but revalidate before every use. no-store means do not write it to disk at all. must-revalidate means once stale do not serve it, even offline. immutable means do not revalidate even on a reload. private means a browser may store it and a shared cache may not. A closing line notes that the two most confused are no-cache, which does store, and no-store, which does not.
    FigureFigure 5. Six directives, and the two that sound the same do opposite things

    no-cache is really please-revalidate, and it is spelled misleadingly. no-store is the one that never writes the response down.

    Two panels. The left, no-cache, shows the response being written to disk then revalidated before use. Its key reads: stored on disk YES, survives logout yes, use it for freshness. The right, no-store, shows the response never written down and refetched every time. Its key reads: stored on disk no, survives logout no, use it for privacy.
    ComparisonFigure 6. no-cache writes the file to the user's disk. no-store is the one that does not

    A bank statement served with no-cache is sitting in the browser profile directory. For anything you would not want a later user of the same machine to recover, the directive is .

    The Mistake That Lasts a Year

    A sequence diagram with three lifelines: a deploy, your server and a user's browser. The deploy ships app.js with max-age of 31536000. The server sends the file, cacheable for a year. An hour later the deploy reports that build was broken and a fix is going out. The user's browser is still using the broken copy and asks nobody, noted as no request means no chance to correct it.
    SequenceFigure 8. The fix deployed in minutes and the users kept the broken file for a year

    Everything that makes a long max-age valuable also makes this happen. A browser holding a fresh entry does not ask, so there is no moment at which you can tell it anything.

    Two panels. The left, one name forever, shows app.js with a short max-age or none, resulting in a request every time. Its key reads: safe to cache long no, fixing a bad build impossible, repeat visit pays a round trip. The right, a name per build, shows app.a1b2c3.js as immutable for one year, resulting in no request at all. Its key reads: safe to cache long yes, fixing a bad build ship a new name, repeat visit free.
    ComparisonFigure 9. Put the content hash in the filename and every hard question here disappears

    A URL that changes whenever its content changes can be cached forever with no risk, because a new build is simply a URL nobody has ever requested. A broken build is fixed by shipping a different name, which every browser must fetch.

    What a Returning Visitor Pays

    A bar chart of network requests on a repeat visit to a thirty asset page. With no caching it is 31 requests. With revalidate everything it is also 31. With hashed filenames and immutable it is 1.
    ChartFigure 11. Revalidating everything makes exactly as many requests as not caching at all

    The responses are smaller and there are just as many of them. On a connection where dominates, which is every mobile connection, the two are close to identical. Request count is what a returning visitor feels, not bytes.

    Four hand-drawn bars. Normal navigation uses the cache normally. Reload revalidates everything, even fresh entries. Hard reload ignores the cache entirely. With immutable, highlighted, reload does not revalidate. A handwritten note reads: your users are not pressing reload, you are, which is why it looks fine.
    SketchFigure 12. Pressing reload tests the opposite of what your users experience

    A reload asks the browser to revalidate things it would otherwise have used without asking. Almost every report that is not working turns out to be a developer pressing the key that disables it. Test by navigating and read the DevTools size column.

    A sequence diagram with four lifelines: browser cache, CDN edge, app cache and database. If the browser copy is fresh it answers with no network at all. Otherwise it asks the nearest edge. On a miss the edge asks the origin, where your code caches too. And only then, the database.

    Choosing, Debugging and a Default

    A flowchart. Starting from a response leaving your server, it asks whether it contains anything specific to one user. If yes, use private with a short max-age, or no-store if sensitive. If no, it asks whether the URL can change when the content does. If yes, use max-age 31536000 with immutable, which is the case you want. If no, it asks how bad serving an old copy for a few minutes would be. If fine, a few minutes of max-age. If not fine, no-cache and accept a round trip on every load.
    FlowchartFigure 14. Four questions, and the second is the one worth engineering for

    The bottom branch, no-cache, is where nervous teams start. It is the most expensive option and the least considered, and the measurement earlier shows what it buys on a small file, which is nothing.

    A flowchart for an asset being re-downloaded every time. If there is a Set-Cookie on the response, some caches refuse to store it, so serve assets from a path the session cookie skips. If Cache-Control is absent, the browser guesses and the guess is usually to revalidate. If the URL carries a changing query parameter, a new query is a new URL and so a new cache entry. Otherwise check Vary, because a header that differs per request means one entry per request.
    FlowchartFigure 15. Four causes, and a missing Cache-Control header is the most common

    Always send an explicit Cache-Control header, even when the answer is a short . Leaving it off does not mean no , it means unpredictable caching that differs between browsers and changes when they update.

    Quick Check

    Knowledge Check

    4 questions - Score 80% to pass

    Q1

    You serve an account page with Cache-Control: no-cache. Is the page written to the user's disk?

    Q2

    A 10 KB asset takes 124.5 ms to download in full. Roughly how long does a 304 revalidation take?

    Q3

    A thirty asset page is visited again by the same user. How many network requests does 'revalidate everything' make compared to no caching at all?

    Q4

    You ship app.js with max-age=31536000 and discover an hour later that the build was broken. What are your options?

    The third row of that terminal is a finding of its own: unpkg sends no ETag at all, so that asset cannot be revalidated even if you wanted to.

    no-store
    Three hand-drawn boxes joined by arrows: memory cache, disk cache and service worker, with the last highlighted. Labels beneath read this tab this session, survives a restart, and your code your rules. A handwritten note reads: only the third one is yours, the first two obey headers and nothing else.
    SketchFigure 7. A browser has several caches, and only the one you write takes instructions

    The memory cache is per tab. The disk cache is the one that holds your mistake for the whole max-age. A service worker is code you shipped, so it can do whatever you decide, and it is the only escape hatch from everything else in this lesson.

    after “The Vocabulary, and the Trap in It”

    Wondering what goes wrong when this gets busy?

    It is taught in full in Write-Back Cache, part of the System Design course.

    You get it for life once you join, with every new lesson included.

    See what the course covers
    A line chart of the percentage of users still on the previous release, against minutes of max-age on the HTML document. It rises from 0 at zero minutes to 40 percent at one minute, 62 at two, 76 at three, 85 at four and 90 at five.
    ChartFigure 10. Assets can be cached forever only because the HTML naming them is not

    The document is the only thing that has to be re-fetched for a release to reach anybody, so its max-age is the propagation delay of every deploy you ever ship. Cache the assets forever and the document barely at all.

    SequenceFigure 13. Four caches in a row, and only the first can answer without a network

    Each layer removes work from the one behind it. The browser is first, so it removes the most, and it is also the one you have least control over. Push work as far left as it can safely go.

    max-age
    Five rows. Too short on a hashed asset: wasted round trips forever, no risk. Too long on a hashed asset: nothing, it cannot serve wrong bytes. Too short on the HTML: slightly more origin load, no risk. Too long on the HTML: your release does not reach anybody. no-cache on an account page: the page is on the user's disk anyway. A closing line reads that the errors are not symmetric, and once names are hashed only two rows can hurt you.
    FigureFigure 16. Five ways to get it wrong, and only two cost you anything real

    Once asset filenames carry a content hash, being too generous with them is free. The remaining risk moves entirely to the document and to privacy, which reshapes the whole decision.

    Five configuration rules. Hashed assets get max-age 31536000 with immutable. Images get max-age 604800. The root and HTML files get no-cache. API paths get no-store. Account and settings pages get private with no-store. A closing line reads that the first line does almost all the work and the third line is what lets it be safe.
    FigureFigure 17. Five rules that cover an ordinary web application completely

    Nothing here is clever. It is the arrangement that follows once you accept that hashed URLs can be cached forever and the document cannot be cached at all. Start from this and change a line only when you can say what the change buys.

    Five rows with real product logos. Chrome DevTools shows disk cache in the Network tab size column, tagged inspect. Firefox exposes about:cache listing disk entries directly, tagged inspect. webpack uses contenthash in the output filename, tagged build. Vite produces hashed names by default with no configuration, tagged build. NGINX needs one location block for hashed assets, tagged serve.
    ArchitectureFigure 18. The build tool decides whether long caching is safe, before any header

    If your build does not hash asset filenames, no Cache-Control header can make long browser caching safe, and the whole discussion is stuck at a few minutes. Fix the filenames first and the headers become trivial.

    Four pairs of sentences. We use no-cache so nothing is stored becomes no-cache stores it, no-store does not. Revalidation is basically free becomes measured, 3.5 percent saved on a 10 KB file. I reloaded and it fetched again becomes reload revalidates on purpose, navigate instead. We can purge the browser cache becomes there is no mechanism, on any browser.
    FigureFigure 19. Four sentences, and the first is a privacy bug rather than a performance one

    Accepting early that the browser cache cannot be reached moves the whole problem into the build, where a content hash solves it permanently and no header ever has to be risky again.

    Six rows summarising the lesson. What it is: a cache on the user's machine, which you cannot reach. The best outcome: no request at all, which no timing table can show. What a 304 saves: the bytes, 3.5 percent on 10 KB and 39.7 percent on 230 KB. The mistake that lasts: a long max-age on a URL that can change content. The fix: put the content hash in the filename. The one short cache: the HTML, because it names everything else.
    FigureFigure 20. The only cache with no purge, and the only one that can answer offline

    Those two facts are the same fact seen from either side. Everything difficult here comes from a URL that can serve different bytes at different times, and removing that possibility in the build turns the browser cache from a risk into the fastest layer you have.

    after “Choosing, Debugging and a Default”

    Reading it is one thing. Explaining it out loud is harder.

    Edge Caching in the System Design course takes it from here.

    It starts from the problem and builds up to the design one step at a time.

    Open the System Design course page $49 once, lifetime access