← Back to DevBytes

Web Storage Quotas: Complete Guide

Introduction to Web Storage Quotas

Web Storage is one of the most fundamental client-side persistence mechanisms available in modern browsers. It allows web applications to store data directly in the user's browser, enabling offline functionality, personalized experiences, and reduced server load. However, browsers do not offer unlimited storage — every origin is subject to a storage quota, a hard limit on how much data can be persisted locally.

Understanding these quotas is essential for any developer building applications that rely on localStorage, sessionStorage, IndexedDB, the Cache API, or the newer File System Access API. Mismanaging storage can lead to silent data loss, broken offline experiences, and frustrated users. This guide walks through everything you need to know about web storage quotas, how to measure them, how to handle limits gracefully, and how to follow best practices.

What Is Web Storage Quota?

A web storage quota is the maximum amount of disk space a browser allocates to a single origin for client-side storage. The quota is shared across several storage APIs, including:

It is important to note that localStorage has its own smaller, separate limit (typically around 5 MB per origin in most browsers), while IndexedDB and the Cache API share a much larger pool that can grow into gigabytes depending on available disk space.

How Browsers Calculate Available Quota

Most modern browsers (Chrome, Edge, Firefox, Safari) calculate the available quota as a percentage of the total free disk space on the user's device. The general rule of thumb is:

These numbers are not standardized and can change between browser versions, so your application should never hard-code assumptions about available space.

Why Storage Quotas Matter

Storage quotas matter because exceeding them does not just cause a write to fail — it can trigger eviction, where the browser proactively deletes stored data to free up disk space. This can happen silently, without any user interaction, and can break your application in subtle ways.

Here are the main reasons quotas deserve careful attention:

Checking Available Storage with the Storage API

The StorageManager interface, accessible via navigator.storage, provides methods to estimate available storage and request persistent storage. The estimate() method returns a promise that resolves to an object containing usage and quota properties.

Basic Usage Example

async function checkStorageEstimate() {
  if (navigator.storage && navigator.storage.estimate) {
    const estimate = await navigator.storage.estimate();
    const usedMB = (estimate.usage / (1024 * 1024)).toFixed(2);
    const quotaMB = (estimate.quota / (1024 * 1024)).toFixed(2);
    const percentUsed = ((estimate.usage / estimate.quota) * 100).toFixed(2);

    console.log(`Used: ${usedMB} MB`);
    console.log(`Quota: ${quotaMB} MB`);
    console.log(`Percent used: ${percentUsed}%`);

    return estimate;
  } else {
    console.warn('StorageManager.estimate() is not supported in this browser.');
    return null;
  }
}

checkStorageEstimate();

The values returned by estimate() are approximate. Browsers intentionally do not provide exact numbers to prevent certain types of fingerprinting attacks. The usage value includes data stored by localStorage, IndexedDB, the Cache API, and service workers for the current origin.

Breaking Down Usage by Storage Type

In newer versions of Chrome, the estimate() method also returns a usageDetails object that breaks down usage by storage mechanism:

async function detailedStorageReport() {
  const estimate = await navigator.storage.estimate();

  if (estimate.usageDetails) {
    const details = estimate.usageDetails;
    console.log('IndexedDB:', details.indexedDB || 0, 'bytes');
    console.log('Cache Storage:', details.caches || 0, 'bytes');
    console.log('Service Worker:', details.serviceWorkerRegistrations || 0, 'bytes');
    console.log('File System:', details.fileSystem || 0, 'bytes');
  } else {
    console.log('Total usage:', estimate.usage, 'bytes');
  }
}

detailedStorageReport();

This breakdown is invaluable when diagnosing which part of your application is consuming the most storage.

Requesting Persistent Storage

By default, browser storage is considered best-effort. This means the browser may evict it when disk space runs low, even if the user has not explicitly cleared data. To protect critical data from eviction, you can request persistent storage using navigator.storage.persist().

async function requestPersistentStorage() {
  if (navigator.storage && navigator.storage.persist) {
    const isPersisted = await navigator.storage.persist();
    
    if (isPersisted) {
      console.log('Persistent storage granted. Data will not be evicted automatically.');
    } else {
      console.log('Persistent storage request was denied.');
    }

    return isPersisted;
  }
  return false;
}

// Check if storage is already persistent
async function checkPersistence() {
  if (navigator.storage && navigator.storage.persisted) {
    const persisted = await navigator.storage.persisted();
    console.log('Storage is persistent:', persisted);
    return persisted;
  }
  return false;
}

Browsers have different policies for granting persistent storage. Chrome may grant it automatically for installed PWAs, while Firefox may prompt the user. Safari's support is more limited. Always handle the case where the request is denied gracefully.

Handling Quota Exceeded Errors

When you exceed the storage quota, different APIs throw different errors. You must handle each one appropriately.

localStorage Quota Exceeded

When localStorage exceeds its limit, the browser throws a QuotaExceededError. You should always wrap writes in a try/catch block:

function safeSetItem(key, value) {
  try {
    localStorage.setItem(key, value);
    return true;
  } catch (err) {
    if (err.name === 'QuotaExceededError' ||
        err.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
      console.error('localStorage quota exceeded. Attempting cleanup...');
      
      // Attempt to remove expired or low-priority items
      cleanupOldEntries();
      
      // Retry once
      try {
        localStorage.setItem(key, value);
        return true;
      } catch (retryErr) {
        console.error('Still unable to write after cleanup.', retryErr);
        return false;
      }
    }
    console.error('Unexpected localStorage error:', err);
    return false;
  }
}

function cleanupOldEntries() {
  // Example: remove items prefixed with "cache:"
  const keysToRemove = [];
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    if (key && key.startsWith('cache:')) {
      keysToRemove.push(key);
    }
  }
  keysToRemove.forEach(k => localStorage.removeItem(k));
}

IndexedDB Quota Exceeded

IndexedDB transactions fail with a QuotaExceededError when the quota is exceeded. The transaction is automatically aborted:

function saveToIndexedDB(db, record) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('records', 'readwrite');
    const store = tx.objectStore('records');
    const request = store.add(record);

    request.onsuccess = () => resolve(request.result);

    request.onerror = () => {
      const error = request.error;
      if (error && error.name === 'QuotaExceededError') {
        console.error('IndexedDB quota exceeded. Cleaning up old records...');
        evictOldRecords(db)
          .then(() => saveToIndexedDB(db, record))
          .then(resolve)
          .catch(reject);
      } else {
        reject(error);
      }
    };
  });
}

function evictOldRecords(db) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction('records', 'readwrite');
    const store = tx.objectStore('records');
    const index = store.index('timestamp');
    const cursorReq = index.openCursor();
    let deleted = 0;
    const MAX_DELETE = 100;

    cursorReq.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor && deleted < MAX_DELETE) {
        cursor.delete();
        deleted++;
        cursor.continue();
      }
    };

    tx.oncomplete = () => {
      console.log(`Evicted ${deleted} old records.`);
      resolve();
    };
    tx.onerror = () => reject(tx.error);
  });
}

Cache API Quota Exceeded

When using the Cache API inside a service worker, writes fail if the quota is exceeded. You should implement an LRU (Least Recently Used) eviction strategy:

async function safeCachePut(cacheName, request, response) {
  const cache = await caches.open(cacheName);
  try {
    await cache.put(request, response);
  } catch (err) {
    if (err.name === 'QuotaExceededError') {
      console.warn('Cache quota exceeded. Running eviction...');
      await evictLeastRecentlyUsedCache();
      await cache.put(request, response);
    } else {
      throw err;
    }
  }
}

async function evictLeastRecentlyUsedCache() {
  const keys = await caches.keys();
  if (keys.length === 0) return;

  // Delete the oldest cache (simple LRU strategy)
  const oldest = keys[0];
  console.log(`Deleting cache: ${oldest}`);
  await caches.delete(oldest);
}

Monitoring Storage Over Time

For applications that store significant amounts of data, it is useful to monitor storage usage over time and warn users before they hit the limit. Here is a utility that periodically checks storage and triggers callbacks at defined thresholds:

class StorageMonitor {
  constructor(options = {}) {
    this.warningThreshold = options.warningThreshold || 0.8;
    this.criticalThreshold = options.criticalThreshold || 0.95;
    this.intervalMs = options.intervalMs || 60000;
    this.onWarning = options.onWarning || (() => {});
    this.onCritical = options.onCritical || (() => {});
    this.timerId = null;
    this.lastLevel = 'ok';
  }

  start() {
    this.check();
    this.timerId = setInterval(() => this.check(), this.intervalMs);
  }

  stop() {
    if (this.timerId) {
      clearInterval(this.timerId);
      this.timerId = null;
    }
  }

  async check() {
    if (!navigator.storage?.estimate) return;

    const { usage, quota } = await navigator.storage.estimate();
    if (!quota) return;

    const ratio = usage / quota;

    if (ratio >= this.criticalThreshold && this.lastLevel !== 'critical') {
      this.lastLevel = 'critical';
      this.onCritical({ usage, quota, ratio });
    } else if (ratio >= this.warningThreshold && this.lastLevel !== 'warning') {
      this.lastLevel = 'warning';
      this.onWarning({ usage, quota, ratio });
    } else if (ratio < this.warningThreshold) {
      this.lastLevel = 'ok';
    }
  }
}

// Usage
const monitor = new StorageMonitor({
  onWarning: (info) => {
    console.warn(`Storage warning: ${(info.ratio * 100).toFixed(1)}% used`);
  },
  onCritical: (info) => {
    console.error(`Storage critical: ${(info.ratio * 100).toFixed(1)}% used`);
    // Show UI prompt asking user to free up space
  },
});

monitor.start();

Best Practices for Managing Web Storage Quotas

1. Choose the Right Storage API

Different storage mechanisms have different characteristics. Selecting the right one prevents unnecessary quota consumption:

2. Always Check Before Writing

Before writing large amounts of data, check the available quota using navigator.storage.estimate(). This prevents unnecessary failed writes and allows you to inform the user proactively:

async function canStore(bytesNeeded) {
  const { usage, quota } = await navigator.storage.estimate();
  const available = quota - usage;
  
  if (available < bytesNeeded) {
    console.warn(`Need ${bytesNeeded} bytes but only ${available} available.`);
    return false;
  }
  return true;
}

// Example: before downloading and caching a large file
async function downloadAndCache(url) {
  const response = await fetch(url);
  const contentLength = parseInt(response.headers.get('Content-Length') || '0', 10);

  if (await canStore(contentLength * 1.2)) { // 20% buffer
    const cache = await caches.open('large-files');
    await cache.put(url, response);
    console.log('File cached successfully.');
  } else {
    console.error('Not enough storage to cache this file.');
  }
}

3. Implement Eviction Strategies

Do not rely on the browser to manage your storage. Implement your own eviction strategies based on your application's needs:

// TTL-based cleanup for localStorage
function setWithExpiry(key, value, ttlMs) {
  const item = {
    value: value,
    expiry: Date.now() + ttlMs,
  };
  safeSetItem(key, JSON.stringify(item));
}

function getWithExpiry(key) {
  const raw = localStorage.getItem(key);
  if (!raw) return null;

  const item = JSON.parse(raw);
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

function purgeExpired() {
  for (let i = localStorage.length - 1; i >= 0; i--) {
    const key = localStorage.key(i);
    const raw = localStorage.getItem(key);
    if (!raw) continue;
    try {
      const item = JSON.parse(raw);
      if (item.expiry && Date.now() > item.expiry) {
        localStorage.removeItem(key);
      }
    } catch {
      // Not a JSON item, skip
    }
  }
}

4. Request Persistent Storage for Critical Data

If your application stores data that the user would consider essential — such as offline documents, unsaved drafts, or financial records — request persistent storage early in the user journey, ideally after the user has demonstrated intent (such as logging in or installing the PWA).

5. Compress Data Before Storing

For large text-based data, consider compressing before storage. Libraries like pako or the built-in CompressionStream API can significantly reduce storage footprint:

async function compressAndStore(key, text) {
  const stream = new Response(text).body
    .pipeThrough(new CompressionStream('gzip'));
  const compressed = await new Response(stream).arrayBuffer();
  
  // Store as base64 in localStorage or as a Blob in IndexedDB
  const base64 = btoa(String.fromCharCode(...new Uint8Array(compressed)));
  safeSetItem(key, base64);
}

async function retrieveAndDecompress(key) {
  const base64 = localStorage.getItem(key);
  if (!base64) return null;

  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }

  const stream = new Response(bytes).body
    .pipeThrough(new DecompressionStream('gzip'));
  const decompressed = await new Response(stream).text();
  return decompressed;
}

6. Communicate with Users

When storage is running low, inform the user clearly. Provide actionable options such as clearing cached data, deleting old downloads, or upgrading to a plan with more server-side storage. Never let quota errors fail silently.

Browser Compatibility Considerations

While the Storage API is widely supported, there are important differences between browsers:

Always feature-detect before using any storage API:

function getStorageCapabilities() {
  return {
    localStorage: typeof localStorage !== 'undefined',
    sessionStorage: typeof sessionStorage !== 'undefined',
    indexedDB: 'indexedDB' in window,
    cacheAPI: 'caches' in window,
    storageEstimate: !!(navigator.storage && navigator.storage.estimate),
    persist: !!(navigator.storage && navigator.storage.persist),
    fileSystemAccess: 'showSaveFilePicker' in window,
  };
}

console.log(getStorageCapabilities());

Conclusion

Web storage quotas are a critical but often overlooked aspect of building robust web applications. By understanding how browsers allocate and enforce storage limits, you can design applications that gracefully handle constrained environments, avoid silent data loss, and provide a reliable experience even when disk space is tight. The key takeaways are to always check available quota before writing large data, implement your own eviction and cleanup strategies, request persistent storage for critical data, compress data where practical, and communicate clearly with users when storage is running low. By following these practices and using the Storage API effectively, you can build web applications that are resilient, trustworthy, and capable of delivering true offline-first experiences across all modern browsers.

— Ad —

Google AdSense will appear here after approval

← Back to all articles