Storage Quotas & Eviction Policies

Modern web applications increasingly rely on client-side persistence to deliver seamless offline experiences. However, browser storage is not infinite, and the mechanisms governing allocation and cleanup are highly dynamic. For frontend engineers and PWA developers, understanding how rendering engines manage disk pressure is critical to preventing silent data loss. This guide is part of Browser Storage Fundamentals & Quotas and details the heuristics behind storage allocation, cross-vendor quota variations, persistence tiers, and production-ready strategies for monitoring and handling eviction events.

Storage eviction decision flow A decision tree showing how a browser decides whether to evict an origin's storage based on disk pressure, persistence grant, and engagement. Disk under pressure? OS reclaim triggered No: data retained quota still available Persistent granted? navigator.storage.persist() Yes: exempt survives until user clears Best-effort: evicted least-recently-used origin first

Understanding Browser Storage Allocation

Modern browsers implement dynamic storage models that scale based on available disk space and user engagement metrics. Unlike legacy fixed-capacity models, contemporary browsers rely on heuristic algorithms to determine how much data an origin can safely persist. These algorithms factor in global device storage, origin visit frequency, and whether the application is installed as a PWA. Engineers must account for these fluid boundaries when designing offline-first architectures, treating available storage as a shared, volatile resource rather than a guaranteed allocation. Proactive quota management should be baked into the application lifecycle, not treated as an afterthought.

The StorageManager interface, exposed at navigator.storage, is the canonical way to query these dynamic boundaries. Its estimate() method returns the current usage and quota for the origin’s temporary storage group, and persist() requests a durability upgrade. Because the quota reported is an estimate that the browser may revise as global free space changes, never cache the value for the lifetime of the session — re-query it before any large write.

Cross-Browser Quota Variations

Storage limits are not standardized across rendering engines. Each vendor applies distinct heuristics, compression strategies, and background throttling rules. For a detailed breakdown of how Browser Storage Limits Across Chrome, Firefox, and Safari differ in practice, consult our comparative analysis. The table below summarizes the headline behaviors engineers most often trip over.

Engine Temporary storage group cap Per-origin behavior Eviction trigger
Chrome / Chromium ~60% of free disk Fraction of shared pool LRU under disk pressure
Firefox ~20% of total disk Lesser of 10 GB or 10% of group LRU; persist() opts out
Safari (WebKit) ~1 GB then prompts Per-origin, user-extendable 7-day inactivity wipe (non-installed)
Edge (Chromium) ~60% of free disk Matches Chrome LRU under disk pressure

Chromium-based browsers typically allocate up to ~60% of available free disk space for the temporary storage group (shared across IndexedDB, CacheStorage, and the Origin Private File System (OPFS)), while Firefox caps the temporary storage group at approximately 20% of total disk space. Safari imposes stricter per-origin caps and aggressively purges data after seven days of user inactivity for non-installed origins — a behavior so disruptive to PWAs that it has its own dedicated playbook in The iOS Safari 7-Day Storage Eviction Workaround. Understanding these variations is critical for cross-platform PWA reliability, especially when caching large media assets or synchronizing offline databases across mobile and desktop environments.

Eviction Tiers and Persistence Guarantees

Browsers categorize stored data into priority tiers to manage disk pressure efficiently. Synchronous key-value stores like localStorage vs sessionStorage fall outside the Storage API quota buckets (they are managed separately), but they are still subject to browser- and OS-level eviction. IndexedDB and the Cache API for Static Assets fall under the temporary (best-effort) storage tier by default; they can request durable storage via the StorageManager API. Properly leveraging the primitives covered in Understanding Web Storage APIs ensures your application requests the appropriate persistence level for critical state. When persistent storage is granted, the origin is exempt from automatic eviction unless the user explicitly clears site data or the device runs completely out of disk space.

Persistence is not a switch you flip unconditionally. Browsers gate navigator.storage.persist() behind engagement signals: an installed PWA, a high site-engagement score, a previously granted notification permission, or a bookmark all raise the odds of a grant. Chromium may auto-grant silently; Firefox shows a permission prompt; Safari requires the request to originate from a user gesture. Always inspect the boolean return value and design a degraded path for denial rather than assuming durability.

Monitoring and Debugging Eviction Events

When disk pressure mounts, browsers silently evict data from non-persistent origins. Proactive monitoring is essential. Learn how to implement quota change listeners and handle QuotaExceededError gracefully by reviewing our guide on Debugging Storage Eviction in Progressive Web Apps. Concurrent read/write operations during eviction windows can corrupt state; use transactions with explicit onerror and onabort handlers on IDBTransaction to isolate and resolve conflicts. Implementing telemetry around storage usage and eviction events allows teams to trigger proactive data pruning before users encounter failures.

Because eviction never throws an error on the read path — a missing database simply opens empty — your code must treat absence as a recoverable state. Add an integrity probe at startup that confirms the expected object stores exist and re-hydrates from the network when they do not. Pairing that probe with periodic estimate() sampling gives you both a leading indicator (rising usage) and a lagging indicator (vanished data) of storage pressure.

Production Implementation Patterns

The following patterns demonstrate how to request persistent storage, monitor quota thresholds, and gracefully handle quota exhaustion in offline-first applications. Implement them in this order: request persistence early, monitor continuously, and fall back deterministically when a write cannot be honored.

  1. Request persistent storage during initialization. Call navigator.storage.persist() once at startup (from a user gesture on Safari) and record the boolean result so later code can branch on durability.
  2. Sample quota before large writes. Call navigator.storage.estimate() and compute the usage ratio; prune proactively above 80% to leave headroom for the pending write.
  3. Wrap writes in quota-aware error handling. Catch QuotaExceededError on the transaction, run a cleanup pass, and fall back to a server sync rather than losing the data.

Requesting Persistent Storage & Monitoring Quota

async function manageStorageQuota(): Promise<void> {
  if (navigator.storage && navigator.storage.persist) {
    const isPersisted = await navigator.storage.persist();
    console.log(`Persistent storage granted: ${isPersisted}`);
  }

  const { usage = 0, quota = 0 } = await navigator.storage.estimate();
  const usagePercent = quota > 0 ? (usage / quota) * 100 : 0;
  console.log(`Storage usage: ${usagePercent.toFixed(2)}%`);

  if (usagePercent > 80) {
    console.warn('Approaching quota limit. Implementing data pruning strategy.');
    await pruneOldCacheEntries();
  }
}

Handling QuotaExceededError Gracefully

async function saveCriticalState(
  db: IDBDatabase,
  data: Record<string, unknown>,
): Promise<void> {
  return new Promise<void>((resolve, reject) => {
    const tx = db.transaction('state', 'readwrite');
    const store = tx.objectStore('state');

    tx.oncomplete = () => resolve();
    tx.onerror = () => {
      const err = tx.error;
      if (err && err.name === 'QuotaExceededError') {
        console.error('Storage quota exceeded. Falling back to server sync.');
        // Kick off background tasks without blocking the current call
        syncToCloudFallback(data).then(() => clearNonEssentialCache());
      }
      reject(err);
    };

    store.put(data, 'critical_state');
  });
}

Concurrency and Lifecycle Notes

Eviction does not respect your transaction boundaries. If the operating system reclaims space while a write is mid-flight, the transaction aborts and onabort fires — distinct from onerror. Always wire both. When multiple tabs share an origin, an eviction or a persist() grant in one tab affects all of them, so coordinate critical writes through the Web Locks API for Cross-Tab Coordination to avoid two tabs racing to repopulate the same evicted store. Treat the post-eviction state as a cold start: re-run your schema check, re-request persistence, and re-sync rather than assuming any prior invariant holds.

Common Pitfalls & Troubleshooting

Avoid these frequent implementation mistakes that lead to data loss, degraded UX, or silent failures in offline-first architectures:

Frequently Asked Questions

What triggers browser storage eviction?

Eviction is primarily triggered by system-wide disk pressure, prolonged inactivity of an origin, or explicit user actions like clearing site data. Browsers prioritize evicting non-persistent, least-recently-used origins first. Granting persistent storage exempts an origin from automatic eviction.

How can I prevent my PWA data from being evicted?

Call navigator.storage.persist() to request durable storage. Browsers typically grant this to PWAs with high user engagement, installed status, or active service workers. Always handle denial gracefully, and on iOS Safari follow The iOS Safari 7-Day Storage Eviction Workaround.

Does localStorage count toward the quota reported by navigator.storage.estimate()?

No. localStorage is managed separately from the Storage API quota bucket. navigator.storage.estimate() reports usage for IndexedDB, CacheStorage, and the Origin Private File System. localStorage has its own roughly 5 MB per-origin limit enforced independently.

How do I monitor remaining storage space programmatically?

Use the StorageManager API via navigator.storage.estimate(). It returns a Promise resolving to an object containing usage (bytes used) and quota (total bytes available) for the current origin’s temporary storage group. Re-query it before large writes rather than caching the value.

Related