Browser Storage Quotas and Management: What Developers Need to Know
Browser storage quotas are the kind of thing that’s invisible until they’re not. An application that stores user data locally works fine in development, works fine in testing, and then silently fails for some users in production because their device is under storage pressure and the browser evicted the app’s data. Understanding how the Storage API, IndexedDB, Cache API, and localStorage relate to each other’s quotas, and how to estimate and request persistent storage, prevents this class of production failure.
The Storage Quota Model
The Storage API specification defines the storage model that modern browsers implement. The key concepts:
Origin storage. Each origin gets its own storage bucket. https://app.example.com and https://other.example.com have separate storage allocations. Everything the origin stores — IndexedDB, Cache API, localStorage, sessionStorage, service worker registrations — counts against the same origin quota.
Global storage limit. The browser has a total storage limit across all origins, based on available disk space. Chrome uses up to 80% of total disk space. Firefox uses up to 50%.
Per-origin limit. Each origin gets a portion of the global limit. The exact allocation varies by browser: Chrome gives each origin up to 60% of the global limit (i.e., up to 48% of total disk space), though practical limits are lower. Firefox caps individual origins at 10GB.
Eviction order. When the browser is under storage pressure, it evicts storage in LRU (least recently used) order, starting with origins the user hasn’t visited recently. Origins with persistent storage permission are not evicted.
The StorageManager API exposes quota information to JavaScript:
const estimate = await navigator.storage.estimate();
console.log(estimate.quota); // bytes available to this origin
console.log(estimate.usage); // bytes currently used
This is approximate — browsers don’t return exact values to prevent fingerprinting.
Storage Mechanisms and What They Count Against Quota
IndexedDB: Counts against origin quota. No practical size limit within quota. Supports transactions, indexes, cursors, and arbitrary structured data. The right choice for significant structured data storage.
Cache API: Counts against origin quota. Used by service workers to cache response objects. Each cached entry includes the request URL, response headers, and response body.
localStorage: Counts against origin quota. Limited to 5MB on most browsers (Chrome: 5MB, Firefox: 5MB, Safari: 5MB). Synchronous API — all reads and writes block the main thread. Appropriate only for small, infrequently-written settings.
sessionStorage: Same quota accounting as localStorage, same size limits. Cleared when the tab or window closes. Not shared across tabs.
Service worker registration and scripts: Counts against origin quota. Usually small.
Cookies: Managed separately from the quota system. Browsers limit cookies per origin (around 180 per domain in Chrome) and cap total cookie storage.
Extension storage (chrome.storage): Extension storage is separate from web origin storage and has its own limits: chrome.storage.local defaults to 10MB (extendable with unlimitedStorage permission), chrome.storage.sync is capped at 100KB total and 8KB per item.
Persistent Storage
By default, origin storage is “best-effort” — the browser can evict it under storage pressure. For applications where data loss is unacceptable (offline-first apps, cached user documents), request persistent storage:
const persisted = await navigator.storage.persist();
if (persisted) {
// storage will not be evicted without explicit user action
}
On Chrome, the browser grants persistent storage automatically if the user has bookmarked the site, added it to the home screen, or the site has push notification permission. On Firefox, the browser prompts the user. On Safari, persistent storage is granted automatically to installed PWAs.
Check the current persistence status:
const isPersisted = await navigator.storage.persisted();
This is a critical distinction for any offline-capable application: if you’re storing user data the user expects to be durable, you need persistent storage, and you should handle the case where it isn’t granted.
IndexedDB Practical Limits and Performance
IndexedDB’s design allows storing large amounts of structured data, but there are practical performance constraints:
Object store design. Each object store in IndexedDB is analogous to a database table. Reading an entire large object store to find a subset of records is slow. Define indexes on the fields you query against:
store.createIndex('by-date', 'timestamp');
const range = IDBKeyRange.lowerBound(yesterday);
const results = await store.index('by-date').getAll(range);
Transaction scoping. IndexedDB transactions lock the object stores they touch. A long-running read transaction blocks writes to the same object stores. Keep transactions short — open, operate, complete. Don’t hold a transaction open across an async operation that doesn’t need it.
Versioning and schema migrations. The upgradeneeded event fires when you open a database with a version number higher than the stored version. All schema changes (creating object stores, adding indexes) must happen in this event handler. Plan your schema versions carefully; you can’t easily remove an object store in a later migration without data loss.
The MDN IndexedDB guide covers the full API with worked examples.
Cache API and Service Worker Caching Strategies
The Cache API is for storing HTTP responses, primarily used in service workers. Common caching strategies:
Cache-first: Check cache, serve if present, fall back to network. Fastest for static assets. Use with versioned URLs to avoid serving stale content.
Network-first: Try network, fall back to cache if network fails. For content that should be fresh but needs offline fallback.
Stale-while-revalidate: Serve cached content immediately, then update the cache from the network in the background. Good for content where freshness is desirable but not critical.
Cache storage grows without bounds unless explicitly managed. Include a cleanup step in the service worker activate event to delete old caches:
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.filter(name => name !== CURRENT_CACHE_VERSION)
.map(name => caches.delete(name))
);
})
);
});
Handling QuotaExceededError
localStorage throws QuotaExceededError synchronously when you exceed the 5MB limit. IndexedDB and Cache API operations reject their promises with a QuotaExceededError when the origin quota is exceeded.
Always wrap storage writes in try/catch or .catch() handlers. Decide whether to:
- Evict old data and retry (appropriate for cache-style storage)
- Inform the user and ask them to manage storage
- Degrade gracefully to in-memory only
For progressive web apps, the Storage Manager estimate can be used to check available space before attempting a large write, reducing the chance of a mid-write failure.
Storage Considerations for Privacy
Browser storage is visible to JavaScript running on the same origin. Content security policies can limit what scripts run, but any script that does run has full access to the origin’s localStorage, IndexedDB, and can respond to the Cache API.
Additionally, browser storage is one of the vectors for tracking: persistent identifiers in localStorage, IndexedDB records keyed by a user identifier, and other long-lived state can all be used as tracking mechanisms. Firefox’s total cookie protection partitions not just cookies but also service worker caches and some storage by top-level site, which affects third-party-embedded applications.
FAQ
What’s the best way to check how much storage my app is using?
navigator.storage.estimate() returns quota and usage. Usage is summed across all storage types for the origin. Individual type breakdowns require querying each API separately (count IndexedDB object stores, enumerate Cache API caches).
Can I increase the storage limit beyond the browser’s default? Not programmatically. The quota is set by the browser based on available disk space. You can request persistent storage to prevent eviction, but you can’t increase the total allocation. For large datasets, server-side storage with local caching is more appropriate.
Does Safari’s ITP affect storage quotas? Safari’s ITP can delete website data for sites the user hasn’t visited recently (the 7-day rule for tracker-classified origins). For sites the user visits regularly, this doesn’t apply. For sites with long user absence, stored data may be deleted. This is separate from the quota mechanism — it’s an ITP-specific eviction policy on top of the quota system.
Is there a way to tell the user that storage is almost full?
Use navigator.storage.estimate() and compare usage to quota. If you’re at 80%+ of your allocation, warn the user in your UI that local storage is limited and offer to clean up or export data. This is better UX than silently failing writes.