OpenPWAStore
Back to News
Guide · May 20, 2026

The Storage Quota API lets PWAs check space before filling it

Guessing storage limits doesn't work. Here's how PWAs can ask before filling space.

OpenPWA Editorial8 min read
The Storage Quota API lets PWAs check space before filling it cover

Why Storage Limits Matter for PWAs

Browsers impose storage limits on web apps to prevent abuse and protect user devices. When you fill up the quota, browsers may start evicting your data—potentially at the worst moment for your user.

The Storage Quota API (navigator.storage) gives PWAs a way to:

  1. Check available space before writing large files or caches
  2. Request persistent storage to reduce eviction risk
  3. Monitor usage to stay within safe limits

MDN's Navigator documentation explains that navigator.storage returns a StorageManager singleton for managing persistence permissions and estimating available storage.

How the Storage Quota API Works

Core Methods

const storageManager = navigator.storage;

// Estimate usage and quota
const { usage, quota } = await storageManager.estimate();

console.log(`Used: ${usage} bytes (${(usage / 1024 / 1024).toFixed(2)} MB)`);
console.log(`Available: ${quota} bytes (${(quota / 1024 / 1024).toFixed(2)} MB)`);
console.log(`Remaining: ${quota - usage} bytes`);

Data returned:

  • usage: Approximate bytes used by your origin (including IndexedDB, Cache API, etc.)
  • quota: Approximate total bytes available to your origin

Requesting Persistent Storage

async function requestPersistentStorage() {
  if (navigator.storage && navigator.storage.persist) {
    const persisted = await navigator.storage.persist();
    console.log(`Storage persisted: ${persisted}`);
    return persisted;
  }
  return false;
}

Note: Calling persist() shows a user permission prompt in many browsers (especially Chrome on desktop). Users can deny it.

Check if Storage is Persisted

const isPersisted = await navigator.storage.persisted();
console.log(`Is storage persisted? ${isPersisted}`);

When to Use the Storage Quota API

Before Caching Large Assets

async function cacheLargeVideo(url) {
  const videoSizeMB = 85; // Estimate
  const { usage, quota } = await navigator.storage.estimate();
  const availableMB = (quota - usage) / 1024 / 1024;

  if (availableMB < videoSizeMB + 10) { // 10 MB buffer
    console.warn('Insufficient storage for offline video');
    // Notify user and offer alternatives
    showStorageWarning();
    return false;
  }

  const cache = await caches.open('video-cache');
  const response = await fetch(url);
  if (response.ok) {
    await cache.put(url, response);
    return true;
  }
  return false;
}

Before Storing User Generated Content

async function saveUserPhoto(file) {
  const { usage, quota } = await navigator.storage.estimate();
  const fileSize = file.size;

  if (quota - usage < fileSize * 1.5) { // 1.5x buffer
    alert('Not enough storage to save this photo. Try freeing up space or using smaller images.');
    return false;
  }

  // Save to IndexedDB...
}

On App Load (Monitoring)

async function checkStorageOnLoad() {
  const { usage, quota } = await navigator.storage.estimate();
  const percentUsed = (usage / quota) * 100;

  if (percentUsed > 80) {
    showStorageWarning(`Used ${percentUsed.toFixed(0)}% of available storage. Clean up to avoid data loss.`);
  }

  return percentUsed;
}

Storage Eviction Rules

Browsers follow eviction policies that prioritize smaller, lightweight origins when storage pressure is high:

Chrome Chromium-based Browsers

MDN and Chrome documentation summarize eviction roughly as:

  1. Eviction happens when the device is critically low on storage
  2. Sat media content (video, audio) is more likely evicted
  3. Origins are evicted unitarily—either all or none for a given origin
  4. Persistence matters: Persisted origins are less likely evicted

Safari

Safari's policy is more aggressive:

  • iOS Safari may clear data from infrequently used apps after ~7 days
  • Desktop Safari enforces stricter per-origin limits
  • Persistence requests are less prominent/hidden

Firefox

Firefox prioritizes based on:

  • Usage quantity
  • Frequency of access
  • Permission grants (storage persistence)

Persistence Strategy Checklist

When to Request Persistence

| Scenario | Request persistence? | Rationale | |----------|---------------------|-----------| | Offline-first PWA with critical user data | ✅ Strongly recommended | Protect against eviction | | Content-heavy app (videos, magazines) | ✅ Yes | Eviction likelihood high | | Simple bookmarking app | ⚠️ Optional | Risk lower | | Multi-user or sharing device | ❌ Avoid | Users may not want persistent storage | | Public kiosk PWA | ❌ No | Not appropriate |

async function requestPersistenceWithExplanation() {
  const persisted = await navigator.storage.persist();

  if (!persisted) {
    // User denied or browser didn't prompt
    showEmail(
      `We prefer persistent storage to protect your data, but we'll work without it. You can enable it in site settings.`
    );
  } else {
    console.log('Storage persisted');
  }

  return persisted;
}

Tips:

  • Explain why persistence matters before requesting
  • Provide an alternative if denied
  • Don't repeatedly prompt the same user in one session

IndexedDB + Storage Quota Integration

IndexedDB Usage Estimates

IndexedDB transactions can be large. Estimate before bulk inserts:

async function bulkInsertRecords(records, dbName, storeName) {
  const { usage, quota } = await navigator.storage.estimate();
  const estimatedSize = records.length * averageRecordSize;

  if (quota - usage < estimatedSize * 1.2) {
    alert('Too many records. Try uploading in batches or deleting older data.');
    return;
  }

  const db = await openDB(dbName);
  const tx = db.transaction(storeName, 'readwrite');
  const store = tx.objectStore(storeName);

  for (const record of records) {
    await store.add(record);
  }
}

IndexedDB + Cache API Co-existence

Both IndexedDB and Cache API share the same storage quota. Your usage includes both:

const { usage } = await navigator.storage.estimate();

// Rough break-down (you must track per-app):
const indexedDBUsage = await trackIndexedDBUsage();
const cacheAPIUsage = usage - indexedDBUsage;

console.log(`IndexedDB: ${indexedDBUsage} bytes`);
console.log(`Cache API: ${cacheAPIUsage} bytes`);

Progressive Web App Specific Considerations

Offline Caches + User Data

For offline-first PWAs, your Cache API for app shell + IndexedDB for user data compete for space:

| Priority | Storage Type | Why | |----------|--------------|-----| | 1 | User-generated content (IndexedDB) | Highest value to user, harder to recreate | | 2 | Critical assets (HTML, CSS, JS in Cache API) | App stability | | 3 | Nice-to-have assets (images, videos) | Lower priority for eviction |

Tip: Periodically clean Cache API of stale resources to free space for IndexedDB.

File System Access + Storage Quota

The File System Access API writes data outside the browser quota ecosystem entirely (user-granted files live on disk), so it doesn't affect quota or usage:

const fileHandle = await window.showSaveFilePicker();
// Writing to this file doesn't count toward navigator.storage

However, for Origin Private File System (OPFS), usage is included:

const root = await navigator.storage.getDirectory();
// Writes here ARE counted in the quota

##实战 Patterns

Pattern 1: Storage-aware Caching

async function smartCache(urls) {
  const { usage, quota } = await navigator.storage.estimate();
  const budgetMB = 100; // Your app's budget (policy)
  const usedMB = usage / 1024 / 1024;

  if (usedMB > budgetMB) {
    console.log('Over budget. LRU-removing oldest caches...');
    await lruEvictCaches(-10); // Free 10 MB
  }

  const cache = await caches.open('dynamic-cache');
  cache.addAll(urls);
}

Pattern 2: Progressive Storage Monitoring

// Monitor every 5 minutes
setInterval(async () => {
  const { usage, quota } = await navigator.storage.estimate();
  const percentUsed = (usage / quota) * 100;

  if (percentUsed > 90) {
    warnUser(`Storage critically full: ${percentUsed.toFixed(0)}% used. Delete old data.`);
  }
}, 5 * 60 * 1000);

Pattern 3: Pre-update Storage Check

async function beforeAppUpdate(newAssets) {
  const { usage, quota } = await navigator.storage.estimate();
  const requiredSpace = newAssets.reduce((sum, asset) => sum + asset.size, 0);

  if (quota - usage < requiredSpace * 1.3) {
    alert('Not enough space to update app. Clear some offline content first.');
    return false;
  }

  return true;
}

Verification Checklist

Before going to production:

  • [ ] navigator.storage.estimate() called before large writes
  • [ ] navigator.storage.persist() requested for critical apps
  • [ ] User sees clear explanations before persistence prompts
  • [ ] Cache cleanup policies are defined (LRU, age-based)
  • [ ] IndexedDB usage is estimated before bulk inserts
  • [ ] Over-budget scenarios show user-friendly warnings
  • [ ] Eviction-prone content is labeled (e.g., "downloaded content")

Browser Compatibility

| Browser | estimate() | persist() | persisted() | |---------|--------------|-------------|---------------| | Chrome | ✅ | ✅ (shows prompt) | ✅ | | Edge | ✅ | ✅ | ✅ | | Firefox | ✅ | ✅ | ✅ | | Safari | ⚠️ Limited | ❌ | ❌ |

Note: Safari does not support persist() or persisted(). Storage limits are enforced more aggressively without user grant.

Debugging Storage

DevTools Application Tab

  • Storage: View IndexedDB, Cache API, local storage quotas
  • Storage Buckets: (Advanced) Fine-grained quota management
  • Clear Storage: For testing eviction and re-caching

CLI/Console Tests

// Quick estimate
navigator.storage.estimate().then(console.log);

// Check persisted
navigator.storage.persisted().then(console.log);

// Test permission
navigator.storage.persist().then(console.log);

Common Pitfalls

1. Assuming Unlimited Storage

Modern browsers enforce per-origin quotas that are finite and eviction-prone.

2. Not Checking Before Large Writes

Writing 50 MB of data without checking storage may fail mid-operation.

3. Ignoring Cache API in Usage

Your usage includes all Cache API caches—they're not free.

4. Forgetting Safari Differences

Safari lacks persistence APIs and may evict more aggressively.

5. Silently Failing Writes

Always handle rejections from IndexedDB/Cache API when quota is exceeded:

try {
  await db.add('myStore', hugeData);
} catch (err) {
  if (err.name === 'QuotaExceededError') {
    showQuotaExceededMessage();
  }
}

Next Steps

  1. Identify high-value user data in your app and request persistence
  2. Add estimate() calls before large IndexedDB writes and cache operations
  3. Implement a cache cleanup policy (LRU, age-based)
  4. Test eviction by filling storage and observing what gets removed first
  5. Document your storage budget and monitor usage in production

Storage isn't infinite. Listen to what the browser tells you before filling it up.