Resolving QuotaExceededError: Browser Storage Limits Across Chrome, Firefox, and Safari
Problem Statement: Cross-Browser QuotaExceededError and Silent Data Loss
Offline-first applications frequently fail during bulk state synchronization or asset caching. Developers encounter QuotaExceededError (DOMException 22) or experience silent data truncation when writing to IndexedDB or the Cache API. The failure manifests inconsistently because Browser Storage Fundamentals & Quotas vary by engine implementation, device capacity, and user interaction state.
Common Symptoms:
DOMException: QuotaExceededErrorthrown duringput(),add(), orCache.put()operations- Silent failure in Safari when exceeding 1GB without the persistent storage flag
- Inconsistent
navigator.storage.estimate()values across Chrome, Firefox, and Safari
Root Cause: Divergent Quota Management & Eviction Strategies
Each browser enforces distinct storage boundaries and lifecycle rules. Chrome allocates dynamic per-origin quotas (typically ~60% of available disk) but aggressively evicts non-persistent data under system pressure. Firefox caps usage at ~10% of disk space per origin and applies strict LRU eviction. Safari imposes a hard 1GB limit per origin, requires explicit user gestures for navigator.storage.persist(), and clears unvisited origins after 7 days. Understanding these mechanics is critical for implementing robust Storage Quotas & Eviction Policies.
Engine-Specific Breakdown:
- Chrome: Dynamic quota, best-effort persistence, aggressive background eviction
- Firefox: Fixed percentage quota, strict LRU, requires
navigator.storage.persist() - Safari: Hard 1GB cap, gesture-gated persistence, 7-day auto-eviction for non-visited origins
Step-by-Step Fix: Quota-Aware Write Implementation
Implement a defensive write pipeline that checks available quota, requests persistence, and handles overflow gracefully before committing data to IndexedDB or Cache API.
-
Query current usage and available quota Call
navigator.storage.estimate()to retrieveusageandquota. Calculate remaining capacity before initiating bulk writes to prevent mid-transaction failures. -
Request persistent storage Invoke
navigator.storage.persist()early in the app lifecycle. Note that Safari requires a direct user gesture (click/tap) to grant this permission. Without it, data remains temporary and subject to automatic eviction. -
Wrap write operations in quota-aware async handler Use
try/catchblocks around database transactions. Implement chunked writes and fallback to in-memory buffers or compressed formats when approaching 80% of available capacity. -
Handle QuotaExceededError explicitly Catch
DOMExceptionwithname === 'QuotaExceededError'orcode === 22. Trigger immediate cache/DB cleanup routines, log telemetry, and notify the user to free disk space or reduce sync frequency.
/**
* Production-safe quota-aware write handler for IndexedDB.
* Handles quota estimation, persistence requests, and explicit DOMException 22 recovery.
*/
async function safePersistData(db, storeName, payload) {
try {
const { usage, quota } = await navigator.storage.estimate();
const remaining = quota - usage;
const payloadSize = new Blob([JSON.stringify(payload)]).size;
// Proactive cleanup at 80% capacity threshold
if (payloadSize > remaining * 0.8) {
console.warn('[Storage] Approaching quota limit. Initiating background cleanup.');
await db.clearOldEntries(storeName);
}
// Request persistent storage if not already granted
if (navigator.storage.persist && !(await navigator.storage.persisted())) {
const granted = await navigator.storage.persist();
if (!granted) {
console.warn('[Storage] Persistent storage denied. Data subject to browser eviction.');
}
}
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
store.put(payload, 'current_state');
// Native promise wrapper for transaction completion
await new Promise((resolve, reject) => {
tx.oncomplete = () => resolve();
tx.onerror = (e) => reject(e.target.error);
});
return { success: true, usage: await navigator.storage.estimate().then(r => r.usage) };
} catch (err) {
// Explicit QuotaExceededError handling (DOMException code 22)
if (err.name === 'QuotaExceededError' || err.code === 22) {
console.error('[Storage] QuotaExceededError triggered. Executing emergency eviction.');
await db.evictLeastUsed(storeName);
throw new Error('Storage quota exceeded. Fallback cleanup executed. Retry with reduced payload.');
}
// Rethrow non-quota errors (e.g., InvalidStateError, DataCloneError)
throw err;
}
}
Validation: Cross-Browser Quota Testing & Verification
Verify implementation stability by simulating low-disk conditions and monitoring quota reporting accuracy across target browsers.
- Confirm
navigator.storage.estimate()returns accurate usage/quota deltas in Chrome DevTools > Application > Storage - Validate Firefox quota behavior by reducing available disk space via VM or container limits (
--storage-opt size=500m) - Test Safari persistence flow by triggering
navigator.storage.persist()exclusively from a user-initiatedclickortouchstarthandler - Simulate
QuotaExceededErrorby artificially inflating payload size; verify fallback cleanup executes without unhandled promise rejections - Monitor browser eviction logs to ensure non-persistent data is cleared predictably under system memory pressure