Choosing a browser storage strategy
Offline support and dependable performance are table stakes for progressive web apps, and even in stable network conditions, smart caching can meaningfully improve the experience. Selecting the right storage mechanism matters just as much. The wrong choice can introduce latency, waste quota, or risk data loss.
First-line options
For most applications, the recommendation is straightforward:
- Use the Cache Storage API for the network resources needed to load the app. It's part of the service worker specification.
- Use the Origin Private File System (OPFS) for file-based content.
- Use IndexedDB for structured data, ideally through a promises wrapper such as
idb.
These three mechanisms are asynchronous, expose APIs through the window object, web workers, and service workers, and are supported across modern browsers. OPFS also offers a synchronous variant limited to web workers.
Mechanisms to use sparingly or avoid
Several legacy or specialized storage APIs still exist but come with restrictions that make them poor defaults.
SessionStorage is scoped to the lifetime of a tab and holds only strings. It's synchronous, which blocks the main thread, and won't work in web workers or service workers. Its practical role is limited to small, session-specific data like an IndexedDB key.
LocalStorage shares the same synchronous and string-only limitations and is likewise unavailable in workers. It's best avoided entirely.
Cookies are transmitted with every HTTP request, so storing any meaningful amount of data inflates each request size. They're synchronous, string-only, and inaccessible from workers.
The File System Access API targets reading and editing files the user explicitly picks. Permission isn't retained across sessions unless you store the file handle in IndexedDB, making it useful mainly for editors or similar workflows. The older File System API and FileWriter API are asynchronous but Chromium-only, which limits their practicality.
Storage quotas by browser
Available space is generous, often hundreds of gigabytes, but varies by browser and device.
- Chrome allows the browser to consume up to 80% of total disk space; a single origin can use up to 60%. Incognito mode drops the origin limit to roughly 5%, and enabling "Clear cookies and site data when you close all windows" cuts it to about 300MB.
- Firefox lets the browser use 50% of free disk space, with an eTLD+1 group capped at 2GB.
- Safari starts near 1GB and prompts the user for more in 200MB increments. Installed PWAs on mobile get an isolated container with no way to request additional space once the quota is reached.
Modern browsers no longer prompt before letting an origin write up to its allotted quota, with Safari's incremental requests being the exception. Exceeding the limit causes writes to fail, so error handling is non-negotiable.
Checking available space
The StorageManager.estimate() method reports the origin's usage and quota across IndexedDB and the Cache API, letting you compute the remaining headroom:
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
const percentUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
Always wrap quota checks in error handling, since the reported quota can exceed what's physically available in edge cases. During development, DevTools storage panes expose current usage and allow manual clearing. Chrome 88 and later can simulate a custom storage quota from the Storage panel, which is useful for testing low-space behavior.
Handling quota exhaustion
Writes to IndexedDB or the Cache API beyond quota throw a DOMException with the name QuotaExceededError. For IndexedDB, the transaction's onabort handler receives the event, and the error is attached to its error property. Cache API writes reject with the same error directly.
Your recovery plan will depend on the app. Common approaches include deleting least-recently-used content, pruning by item size, or giving users an explicit cleanup UI.
Eviction and persistence
Storage falls into two buckets: Best Effort and Persistent. Best-effort data may be cleared automatically when the browser runs low on space; it's evicted origin-by-origin beginning with the least recently used. Persistent storage requires user action in browser settings to clear.
By default, site data is treated as best effort. Sites sharing data across an origin can request persistent storage to protect against eviction. Note that Safari applies a separate rule: since iOS 13.4 and Safari 13.1, script-writable data like IndexedDB, service worker registrations, and Cache API content is evicted after seven days of non-interaction. Installed home-screen PWAs are exempt from this policy.
The Storage Buckets API introduces another option: creating multiple buckets for one origin so the browser can evict them independently. That gives you direct control over eviction priority and lets you shield high-value data.
Wrappers and specialized engines
Raw IndexedDB is low level, event-based, and demands a lot of setup just to persist a small object. Libraries like idb hide transactions and schema versioning behind a promise-based interface while exposing the underlying capabilities when you need them.
For relational workloads, SQLite wasm is available as a successor to the removed Web SQL spec. It runs in the browser and can back SQLite databases with OPFS, documented by Google in their SQLite Wasm announcement.
What storage limits mean in practice
The practical takeaway for web developers has shifted dramatically. The old constraints—small quotas and constant prompts asking users to free up space—are largely gone. Modern browsers give sites access to effectively all local storage they need to run their applications.
The StorageManager API provides the tool to inspect what is available. Calling estimate() on the storage manager gives you two key values: quota, the total space available to your origin, and usage, the amount currently used. This is the primary mechanism for monitoring consumption against the budget the browser provides.
The other critical piece is persistent storage. By default, browsers may evict stored data when the device runs low on space, using a least-recently-used strategy across all sites. Requesting persistence for your origin changes that equation: once granted, your data is safe from automatic eviction. The only ways it can be removed are if the user explicitly clears it from browser settings, or if the site itself deletes it.
Resources for digging deeper
For those building data-heavy applications, two references are worth keeping close:
- IndexedDB Best Practices—covers the fundamental patterns for the most common client-side database.
- Chrome Web Storage and Quota Concepts—details the internal accounting and eviction rules used in Chromium browsers.
The final piece of guidance is to build against reality, not against the old max-quota rules. The default storage budget for an origin is typically a percentage of the total disk space available, but this varies between engines and can depend on whether the storage is used on a hard drive or a solid-state drive. Safari's limits differ again, though it does not enforce a hard cap on local storage in the same way localStorage used to. Measure with estimate() at runtime instead of hard-coding assumptions, watch for the eviction events where the platform exposes them, and always handle the case where persist() is rejected by the user.



