JavaScript Storage Manager Table

PieceWhat it doesField note
navigator.storage.estimate()The quota desk{usage, quota} for the whole origin pool (IndexedDB + caches + OPFS + storage); numbers deliberately fuzzy - anti-fingerprinting rounds or pads them
persist() + persisted()The durability handshakeask after meaningful user action (installed PWA, notification permission, engagement are the protected classes), then verify
best-effort evictionThe default threatunder disk pressure the browser clears non-persisted origins it deems unused; persisted data survives pressure (never manual clears)
per-origin poolThe scope truthquota is per origin, subdomains do not share it; two apps on one disk each think they own most of it
QuotaExceededErrorThe failure surfaceIndexedDB transactions fail, caches and OPFS reject their own quota errors; a silent catch is the lost-drafts bug - show state, offer cleanup, retry
usage as inventoryThe self-auditversion caches, delete stale ones on boot, treat 90% of quota as full; the browser rewards tidy origins and evicts hoarders
the fuzzy numbersThe honesty clausepresent buckets (plenty / getting full / critical) not exact bytes; count your own records for precise accounting; quota can shrink when the disk fills
support truthThe matrixmissing on insecure contexts and older Safari; OPFS and Storage Buckets are newer still - feature-detect the desk before promising durability
Reference: the MDN Storage API. The quota desk for the whole origin: one pool, one estimate, one persistence plea - the only place the browser tells you how full you are and how safe your data is.
Bottom line: design against the contract, not the bytes. Check estimate() before large writes, catch every quota-shaped error as a first-class UI state, clean your own caches when usage climbs, and call persist() only when you can argue merit - after install, after real work is saved. Never promise more durability than persisted() confirms.
Related tools: the IndexedDB table (the big tenant of the pool), the service worker table (the scope CacheStorage lives in), the file system access table (the OPFS corner), the localStorage table (the small synchronous shelf), and the PWA install table (the protected class that gets persist granted).

The Storage API is the quota desk for everything your origin stores: navigator.storage.estimate() returns how much you are using ({usage, quota}), persist() asks the browser to stop evicting your data under pressure, and persisted() tells you whether that plea was granted. IndexedDB, CacheStorage, origin-private file system - every storage area on the origin shares this single pool, and this API is the only place the browser tells you how full it is and how safe you are.

Bottom line: web storage is best-effort by default, and the quota desk is how you graduate. A fresh origin typically gets a share of free disk (Chrome's rule of thumb: a fraction of actual disk, computed per origin), but under storage pressure the browser may evict data from origins it deems unused - unless your data is persisted, or the site is installed (PWA), high-engagement, or granted notification permission. persist() is the handshake: ask, check persisted(), and only then promise your users their drafts and offline data will still be there.

The honest part: estimate() numbers are deliberately fuzzy (some browsers round or pad them to fight fingerprinting) and the quota is per-origin, not global - two apps on one disk each think they have most of it. Design against the contract, not the exact bytes: treat quota errors as first-class UI states, clean up your own old caches when usage climbs, and never promise more durability than persisted() confirms.

How to use

  1. Read the desk: const {usage, quota} = await navigator.storage.estimate(); - usage covers IndexedDB + caches + OPFS + storage for the origin; log it (divide by 1048576 for MiB) and surface a warning to users before the write fails, not after.
  2. Ask for durability: const granted = await navigator.storage.persist(); then verify with navigator.storage.persisted() - persist() needs a reason to be granted (installed PWA, notification permission, engagement); call it after the user does something meaningful, not on load.
  3. Handle the failure mode: quota writes throw QuotaExceededError (IndexedDB) or reject - catch, show the state, offer a cleanup action (drop old caches, purge abandoned drafts), then retry; a silent catch here is the classic lost-drafts bug.
  4. Clean your own room: usage is your inventory - version your caches, delete stale ones on boot, and audit in DevTools Application panel; the browser rewards tidy origins and evicts hoarders under pressure.
  5. Scope the promise honestly: quota is per-origin (subdomains do not share it), estimate() may be rounded, and eviction never touches persisted data - state in your UI what is true: 'saved on this device, safe while this app stays installed' beats 'saved to the cloud' claims that storage never made.

Frequently asked questions

How much storage does a web app actually get?

It depends on the engine and the disk, and the honest answer is: ask, do not assume. Chrome-family computes the quota from real free disk space - the origin's share grows with available space and can be substantial (gigabytes on a healthy disk, with a global cap across origins); Firefox grants up to a fixed fraction (10% of disk, capped); Safari has historically been stingiest, especially for non-installed web content. estimate() reports what the browser will admit to ({usage, quota, usageDetails in some engines}), and those numbers can be deliberately imprecise. The design rule that survives all of them: scale to usage, not to quota - keep your working set small, check estimate() before large writes, and treat the quota number as a hint that can shrink when the user's disk fills.

What does persist() actually protect my data from?

From best-effort eviction - the browser's right to clear storage from origins under disk pressure or after long disuse. Without persistence, Chrome-family browsers may evict origin storage when the device runs low, prioritizing data from sites the user actually engages with (installed PWAs, sites with notification permission, bookmarked/high-engagement sites are the protected classes). With persist() granted, your IndexedDB, CacheStorage and OPFS survive pressure; they still lose to the user manually clearing site data, and storage may still be cleaned when a user never returns for extreme periods (implementation-specific). So persisted() changes the durability promise from 'probably there' to 'protected by policy' - which is exactly the difference between offline-capable apps that work and ones that silently lose their data caches. Note the grant heuristics: call persist() when you can argue merit (after install, after the user saves real work), because a call on first paint has nothing to show for itself.

Why did estimate() give me weird numbers, or none at all?

Anti-fingerprinting. Storage usage is one of many high-entropy signals - your exact bytes stored plus exact quota uniquely identify a browser profile - so engines round or pad the values (Firefox rounds both usage and quota; Chrome exposes usageDetails only in some contexts). The API can also be missing entirely on insecure contexts and older Safari. Practical handling: present storage to humans as buckets (plenty / getting full / critical) instead of exact byte readouts, compute thresholds as percentages of quota, and cache nothing across sessions based on absolute byte values. If you need precise accounting of YOUR data, count it yourself as you write (sum your record sizes in your own metadata) - the browser's estimate is for capacity planning, not for invoicing.

How do quota errors actually surface, and what should the UI do?

Per storage area: IndexedDB transactions fail with QuotaExceededError, the origin file system and CacheStorage APIs reject with their own quota-flavored errors - and if you swallow them, writes start failing silently and users lose work hours later. The professional loop: before large writes, check estimate(); during writes, catch every quota-shaped rejection and mark the app 'storage full'; in that state, offer a real remediation panel (list your caches with sizes and ages, let the user purge old ones - you built them, you have the keys) and retry after cleanup. Two extras that separate good implementations: reserve headroom by treating 90% of quota as full, and remember eviction can also shrink your quota downward over time - the error can arrive even when your usage did not change, because the disk filled up. Quota is a shared resource on a device you do not own; the UI should say so.

Related tools