Cache API & file storage
Store whole responses with the Cache API, inspect browser storage quota, request persistence, and write private origin files with OPFS.
- 01Store responses deliberatelyUse CacheStorage and named Cache objects for Request to Response pairs.
- 02Read storage limits honestlyInspect approximate quota, usage, eviction, and persistence decisions.
- 03Write private filesUse OPFS handles for origin-private folders and files when the browser supports them.
Responses, quota, and private files
Browser storage is not one toolbox. It is a set of very different tools. The Cache API stores whole Request → Response pairs. The Storage Manager tells you roughly how much space your origin is using and whether it has persistent storage. The Origin Private File System, usually shortened to OPFS, gives your site private file and folder handles.
Those APIs are useful when an app should feel fast or keep working offline: documentation readers, drawing tools, recipe apps, music editors, dashboards, and installable web apps. They are also easy to misuse if you treat all storage as “a place to put stuff.” In this lesson we choose storage by the shape of the data and the lifetime you need.
You can cook once, store the whole meal, and grab it later. The network is the store. A named Cache is the shelf. You decide what goes in and when stale entries leave.
- In real life: Ready meal
- In JavaScript: A complete
Responseincluding headers and body - In real life: Label on the container
- In JavaScript: The
Requestor URL used as the key - In real life: Pantry shelf
- In JavaScript: A named
Cacheopened withcaches.open() - In real life: Throwing out old food
- In JavaScript: Your versioning and cleanup code
Where the analogy stops: A pantry does not have streaming response bodies or browser quota. Also, unlike the automatic HTTP cache, the Cache API never expires entries just because time passed.
Definition: CacheStorage is the browser’s collection of named Cache objects; each Cache maps requests to responses. OPFS is a private file area for the same origin. Quota is the browser’s estimate of how much room that origin may use.
The Cache API
MODELThe global caches object is a CacheStorage. It is available in windows and workers in supporting secure contexts; http://localhost counts as secure for development. You call caches.open(name) to get a named Cache, then use methods such as put, add, match, keys, and delete.
| Question | Cache API | HTTP cache |
|---|---|---|
| Who controls entries? | Your JavaScript code opens named caches and writes entries. | The browser follows request and response cache rules. |
| What is stored? | Request to Response pairs you choose. | Network responses the browser decides are reusable. |
| Expiration | No automatic expiration. Use versioned cache names and delete old entries. | Headers such as Cache-Control guide reuse and revalidation. |
| Typical user | Service workers and offline-capable app code. | Every page load and subresource request can use it automatically. |
const cache = await caches.open("cf-demo-pantry");await cache.put( "/cf-demo/menu.json", new Response(JSON.stringify(menu), { headers: { "Content-Type": "application/json", "X-Saved-At": savedAt }, }),);await cache.add(location.href);const match = await cache.match("/cf-demo/menu.json?from=button", { ignoreSearch: true });const allKeys = await cache.keys();await cache.delete("/cf-demo/menu.json");The value is a real Response. Headers come with it. Bodies are streams, so if you fetched a response and also want to read it in your code, clone it before one copy is consumed. Opaque responses can be cached too, but scripts cannot inspect their contents and browsers may charge padded quota space for privacy.
It is excellent for replaying responses. It is awkward for searching records, partial updates, and queries. Structured records belong in IndexedDB; file-shaped drafts belong in OPFS or IndexedDB.
Pantry playground
INTERACTIVEThis playground uses the real Cache API when your browser exposes caches. It creates a demo cache named cf-demo-pantry, stores a synthetic JSON response, stores this same-origin page with cache.add(location.href), matches with ignoreSearch, lists keys, proves caches.match searches across named caches, and deletes the demo data.
const cache = await caches.open("cf-demo-pantry");await cache.put( "/cf-demo/menu.json", new Response(JSON.stringify(menu), { headers: { "Content-Type": "application/json", "X-Saved-At": savedAt }, }),);await cache.add(location.href);const match = await cache.match("/cf-demo/menu.json?from=button", { ignoreSearch: true });const allKeys = await cache.keys();await cache.delete("/cf-demo/menu.json");- 1
Press a button to use the real Cache API in this browser.
The cache name starts with cf-demo and Reset deletes it. CacheStorage usually requires a secure context; localhost counts.
Reset deletes the demo cache. The component also deletes it on unmount. That cleanup matters: educational demos should not leave mystery storage behind.
Cache-first and stale refresh
STEP THROUGHService workers often pair the Cache API with request strategies. You can use the same ideas in app code: try the cache first, fall back to the network, or serve something cached while refreshing it for the next visit.
async function getMenu(cache, network, mode) { const cached = cache.match("/menu.json"); if (cached && mode === "cache-first") { return { source: "cache", menu: cached.menu }; } if (cached && mode === "stale-while-revalidate") { network.fetch("/menu.json").then((fresh) => { cache.put("/menu.json", fresh); }); return { source: "cache", menu: cached.menu }; } const fresh = await network.fetch("/menu.json"); cache.put("/menu.json", fresh); return { source: "network", menu: fresh.menu };}Switch strategy, then step through how cached data changes the control flow.
script
const cached = cache.match("/menu.json"); if (cached && mode === "cache-first") { return { source: "cache", menu: cached.menu }; } if (cached && mode === "stale-while-revalidate") { network.fetch("/menu.json").then((fresh) => { cache.put("/menu.json", fresh); }); return { source: "cache", menu: cached.menu }; } const fresh = await network.fetch("/menu.json"); cache.put("/menu.json", fresh); return { source: "network", menu: fresh.menu };}Cache-first is great for versioned files and app shells where cached is usually correct. Stale-while-revalidate is great when speed matters and yesterday’s data is acceptable for a moment. It is a poor fit for bank balances, checkout prices, or anything where old data can harm the user.
Storage quotas and persistence
INTERACTIVEYour site gets room based on the device and browser policy. Asking for persistence is like asking the landlord not to clear your things when the building is crowded.
- In real life: Apartment size
- In JavaScript: The origin's available quota
- In real life: Stuff already inside
- In JavaScript: Current usage
- In real life: Landlord promise
- In JavaScript: Persistent storage request
- In real life: Clearing space
- In JavaScript: Eviction or user-cleared site data
Where the analogy stops: Browsers do not promise a fixed number of bytes forever. Quota is approximate, varies by browser and disk conditions, and is deliberately imprecise for privacy.
navigator.storage.estimate() returns approximate usage and quota. Some browsers include usageDetails. navigator.storage.persisted() reports whether the origin is already persistent, and persist() asks for persistence. Depending on the browser, that request may be granted automatically, prompt the user, or be denied.
const estimate = await navigator.storage.estimate();const persisted = await navigator.storage.persisted();// Only ask after a user click; some browsers prompt here.const granted = await navigator.storage.persist();const estimate = await navigator.storage.estimate();const persisted = await navigator.storage.persisted();// Only ask after a user click; some browsers prompt here.const granted = await navigator.storage.persist();- 1
Press Estimate to ask the Storage Manager for approximate usage and quota.
Quota numbers are approximate and can change. Persistence may be granted automatically, prompt, or be denied depending on the browser.
Persistence reduces automatic eviction under storage pressure; it is not a backup system. Users can still clear site data, private browsing can be temporary, and your app should rebuild caches when needed.
The Origin Private File System
INTERACTIVEOPFS gives your app file-shaped storage without asking the user where to save each internal file. That is useful for drafts, media chunks, local project files, and generated assets.
- In real life: Private folder
- In JavaScript: The origin's OPFS root
- In real life: Subfolders
- In JavaScript:
getDirectoryHandle(name, { create: true }) - In real life: Text or binary files
- In JavaScript:
getFileHandle,createWritable,getFile() - In real life: Locked door
- In JavaScript: Only the same origin can see it
Where the analogy stops: It is not the user's normal Downloads folder. The files are private to the origin and can be removed when site data is cleared. Browser support and main-thread writable handles vary.
The entry point is navigator.storage.getDirectory(), which returns the OPFS root FileSystemDirectoryHandle. In supporting browsers you can create a file, call createWritable(), write text or bytes, close the stream, read with getFile().text(), iterate entries, and delete with removeEntry. Sync access handles are for dedicated workers; this page demonstrates the main-thread writable flow only when available.
const root = await navigator.storage.getDirectory();const demo = await root.getDirectoryHandle("cf-demo", { create: true });const file = await demo.getFileHandle("menu-note.txt", { create: true });const writable = await file.createWritable();await writable.write("Cache pantry inspected at lunch.");await writable.close();const text = await (await file.getFile()).text();for await (const [name, handle] of demo.entries()) { console.log(name, handle.kind);}await root.removeEntry("cf-demo", { recursive: true });const root = await navigator.storage.getDirectory();const demo = await root.getDirectoryHandle("cf-demo", { create: true });const file = await demo.getFileHandle("menu-note.txt", { create: true });const writable = await file.createWritable();await writable.write("Cache pantry inspected at lunch.");await writable.close();const text = await (await file.getFile()).text();for await (const [name, handle] of demo.entries()) { console.log(name, handle.kind);}await root.removeEntry("cf-demo", { recursive: true });- 1
Press Write file to try OPFS in this browser.
Files live under a private cf-demo folder for this origin and are removed on Reset and unmount.
Which storage should I choose?
SORTThe fastest way to choose is to ask what shape the data has. Is it a response? A private file? A tiny string preference? A queryable collection? A credential JavaScript should not read?
function chooseStorage(need) { if (need === "offline app shell") return "Cache API"; if (need === "large private file") return "OPFS or IndexedDB"; if (need === "small UI preference") return "localStorage"; if (need === "queryable records") return "IndexedDB"; return "HttpOnly cookie";}- Offline app shell: HTML, CSS, icons, JSON responses
- A same-origin avatar
Responseyou want offline - Large private draft video or drawing file
- Thousands of records you need to search by index
- Theme = dark
- Temporary wizard step for this tab
- Session token that JavaScript should not read
- Server credential sent automatically with same-site requests
Sort each real app need into the storage option that fits best.
| Need | Good fit | Why |
|---|---|---|
| Offline app shell or replayable response | Cache API | Stores whole responses with headers. |
| Large drafts or binary app files | OPFS or IndexedDB | Private storage designed for larger app data. |
| Small UI preference | localStorage | Simple string key-value data. |
| Structured queryable records | IndexedDB | Object stores, indexes, and transactions. |
| Session token | HttpOnly cookie | JavaScript cannot read it. |
Where you will use this
REAL APPSImagine a restaurant menu editor. The public app caches the menu JSON and shell files so repeat visitors see the menu instantly. The editor estimates quota before saving large image drafts. Draft notes and generated images live in OPFS under a project folder. The user’s theme choice is just a tiny localStorage value. Their login token belongs in an HttpOnly cookie, not in script-readable storage.
A service worker can answer requests from the Cache API while the page is offline. The page can use IndexedDB or OPFS for app data. The Service workers & offline apps lesson covers the interception part; this lesson focuses on the storage APIs themselves.
Professional code also includes cleanup. Name caches with versions, remove old versions, cap large OPFS projects, and rebuild derived caches if they disappear. Treat browser storage as helpful local state, not your only copy of critical user data.
Common misconceptions
WATCH OUT- “The Cache API is the HTTP cache.” They are separate. Your code manages named Cache objects.
- “Cache API entries expire by themselves.” They do not. Use versions and cleanup.
- “A cached Response can be read forever.” Stored responses can be matched again, but each Response body stream you hold is still one-use. Clone before two reads.
- “Quota is guaranteed storage.” It is approximate and can change. Non-persistent data may be evicted.
- “persist() always shows a permission prompt.” Browsers vary: automatic grant, prompt, or denial are all possible.
- “OPFS files show up in the user’s file browser.” They are private to the origin and are removed with site data.
Practice exercises
5 EXERCISESPredict the header value printed by this cache-like Map.
const response = new Response("hello", { headers: { "X-Saved-At": "noon" } });
const cache = new Map();
cache.set("/menu", response.clone());
console.log(cache.get("/menu").headers.get("X-Saved-At"));const response = new Response("hello", { headers: { "X-Saved-At": "noon" } });
const cache = new Map();
cache.set("/menu", response.clone());
console.log(cache.get("/menu").headers.get("X-Saved-At"));The cloned Response stored in the fake cache keeps its headers, so the log is noon.
What two lines print after cloning the Response?
const response = new Response("one-use body");
const copy = response.clone();
response.text().then((text) => console.log(text));
copy.text().then((text) => console.log(text));const response = new Response("one-use body");
const copy = response.clone();
response.text().then((text) => console.log(text));
copy.text().then((text) => console.log(text));Both the original and the clone contain the same text, so both lines print one-use body.
A teammate says, “The Cache API will remove old entries after a day.” Who actually owns that cleanup?
Your app code must remove old Cache API entries. The browser may evict site data under pressure, but it does not apply an automatic age limit to named Cache entries.
Which method estimates origin storage usage and quota?
Call navigator.storage.estimate() to get approximate usage and quota values.
Which method opens the OPFS root directory?
Use navigator.storage.getDirectory() to open the origin-private root, then create folder and file handles inside it.
Check your understanding
8 QUESTIONSQuestion 1 of 8What does a named Cache store?
Choose an answer to see the explanation.
Question 2 of 8What does this print?
Read the code, then predictconst response = new Response("hello", { headers: { "X-Saved-At": "noon" } }); console.log(response.headers.get("X-Saved-At"));Choose an answer to see the explanation.
Question 3 of 8Which statement about Cache API expiration is true?
Choose an answer to see the explanation.
Question 4 of 8What does the Cache API availability check print in Node tests?
Read the code, then predictconsole.log(typeof caches === "undefined" ? "no Cache API" : "Cache API available");Choose an answer to see the explanation.
Question 5 of 8What does
navigator.storage.estimate()return?Choose an answer to see the explanation.
Question 6 of 8Which API opens the OPFS root?
Choose an answer to see the explanation.
Question 7 of 8Why clone a Response before caching it and reading it?
Choose an answer to see the explanation.
Question 8 of 8Which storage fits a secret session token best?
Choose an answer to see the explanation.
Key takeaways
- The Cache API stores named
Request→Responsepairs and does not expire them automatically. - Clone responses before caching and reading when two consumers need the same body.
navigator.storage.estimate()reports approximate usage and quota;persist()asks for reduced eviction risk.- OPFS gives private origin files and folders through handles, with browser support and cleanup responsibilities to check.
- Choose storage by data shape: responses, files, tiny preferences, records, or credentials.
Final definition: Cache API stores replayable responses, Storage Manager describes the origin’s space, and OPFS stores private files for apps that need file-shaped data.
Up next: Observers — React to visibility, DOM, and size changes efficiently.