Watching Storage Usage with getBytesInUse

Measure chrome.storage usage before it hits quota: getBytesInUse per key and per area, sync quotas, warning thresholds, finding the keys that grow, and showing users how much space the extension uses.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

The first sign of a storage problem is usually an error from a user’s machine: “QUOTA_BYTES_PER_ITEM quota exceeded” from chrome.storage.sync, or writes to local failing after a year of accumulated history. By then the user has lost data. chrome.storage exposes exactly the information needed to see it coming — getBytesInUse reports how many bytes a key, a set of keys, or a whole area occupies — and a few lines of monitoring turn quota exhaustion from an incident into a warning. This guide shows how. It belongs to chrome.storage API and sync.

What the numbers mean

chrome.storage.<area>.getBytesInUse(keys) returns the size of the given keys, or of the whole area when called with null. The size is measured as the JSON serialisation of each value plus the length of its key — the same measure the quotas use. The quotas themselves are exposed as constants: chrome.storage.sync.QUOTA_BYTES (102,400 bytes total), QUOTA_BYTES_PER_ITEM (8,192 bytes), MAX_ITEMS (512) and write-rate limits; chrome.storage.local.QUOTA_BYTES (10,485,760 bytes, unless the extension has unlimitedStorage); and chrome.storage.session.QUOTA_BYTES (10 MB in recent Chrome). Comparing usage with those constants tells you how close each area is to its ceiling.

chrome.storage quotas by areaTotal quota in kilobytes for chrome.storage.sync, the per-item limit in sync, chrome.storage.local without unlimitedStorage, and chrome.storage.session in Chrome.sync per item8 KBsync total100 KBlocal (no unlimitedStorage)10240 KBsession10240 KB
sync is two orders of magnitude smaller than local — and limited per item as well.

Step-by-step: monitor before it fails

1. Read usage for every area

 1// sw.js
 2export async function usageReport() {
 3  const areas = {
 4    sync: chrome.storage.sync,
 5    local: chrome.storage.local,
 6    session: chrome.storage.session,
 7  };
 8  const report = {};
 9  for (const [name, area] of Object.entries(areas)) {
10    if (typeof area?.getBytesInUse !== "function") continue;
11    const used = await area.getBytesInUse(null);
12    const quota = area.QUOTA_BYTES ?? null;
13    report[name] = { used, quota, pct: quota ? Math.round((used / quota) * 100) : null };
14  }
15  return report;
16}

Execution context: the service worker or any extension page. QUOTA_BYTES is undefined for local when the extension declares unlimitedStorage in some versions, and very large in others; treat a missing quota as “no practical limit” and watch growth instead. Feature-detecting getBytesInUse keeps the code working in engines that do not implement it for every area.

2. Find the keys that grow

1export async function largestKeys(area = chrome.storage.local, top = 10) {
2  const all = await area.get(null);
3  const sizes = await Promise.all(Object.keys(all).map(async (k) => [k, await area.getBytesInUse(k)]));
4  return sizes.sort((a, b) => b[1] - a[1]).slice(0, top);
5}

Execution context: the service worker, typically from a debug page or on a schedule. Total usage tells you there is a problem; per-key usage tells you where. In practice one or two keys — an ever-growing history array, a cache that is never pruned — account for most of the space. Calling getBytesInUse per key is cheap for a few hundred keys; for thousands, compute sizes with new Blob([JSON.stringify(value)]).size as an approximation.

A weekly storage health checkAn alarm triggers the check; the worker reads usage per area, compares with quotas, lists the largest keys if above a threshold, prunes known caches, and records a warning for the options page.Weekly alarmstorage-healthusageReport()per area> 70% of quota?thresholdif over the thresholdlargestKeys()find growthPrune cachesowned dataWarn in optionsif still high
Measure, prune what you own, and tell the user only if pruning is not enough.

3. Check sync writes against per-item limits before writing

 1export async function safeSyncSet(key, value) {
 2  const bytes = new TextEncoder().encode(key + JSON.stringify(value)).length;
 3  if (bytes > chrome.storage.sync.QUOTA_BYTES_PER_ITEM) {
 4    throw Object.assign(new Error(`${key} is ${bytes} bytes; sync limit is ${chrome.storage.sync.QUOTA_BYTES_PER_ITEM}`), { code: "too-large" });
 5  }
 6  const used = await chrome.storage.sync.getBytesInUse(null);
 7  const current = await chrome.storage.sync.getBytesInUse(key);
 8  if (used - current + bytes > chrome.storage.sync.QUOTA_BYTES) {
 9    throw Object.assign(new Error("sync storage would exceed its total quota"), { code: "quota" });
10  }
11  await chrome.storage.sync.set({ [key]: value });
12}

Execution context: any trusted context. Checking before writing turns a rejected write — whose data is simply lost — into an error your code can handle, for example by falling back to local and telling the user that this setting will not sync. Measuring with TextEncoder counts bytes, matching how the quota is measured for non-ASCII text.

4. Run a scheduled health check

 1chrome.alarms.create("storage-health", { periodInMinutes: 7 * 24 * 60 });
 2chrome.alarms.onAlarm.addListener(async ({ name }) => {
 3  if (name !== "storage-health") return;
 4  const report = await usageReport();
 5  for (const [area, r] of Object.entries(report)) {
 6    if (r.pct !== null && r.pct >= 70) {
 7      await pruneOwnedCaches(area);
 8      const after = (await usageReport())[area];
 9      if (after.pct >= 70) await chrome.storage.local.set({ storageWarning: { area, pct: after.pct, at: Date.now() } });
10    }
11  }
12});

Execution context: the service worker, with the alarm listener at the top level. Seventy per cent leaves room to act before writes fail. Prune what the extension owns and can regenerate — caches, old logs, expired drafts — automatically; surface a warning for anything that is user data, so the user decides what to delete.

What to do at each usage levelRecommended actions when a storage area is below 50 percent, between 50 and 70, between 70 and 90, and above 90 percent of its quota.UsageAutomatic actionUser-visible< 50%NoneNothing50–70%Prune caches weeklyNothing70–90%Prune nowNote in options> 90%Stop non-essential writesWarning + clean-up UI
Act early on what you own; involve the user before writes fail.

5. Show usage to the user

1// options.js
2const report = await chrome.runtime.sendMessage({ type: "storage:report" });
3const fmt = new Intl.NumberFormat(undefined, { style: "unit", unit: "kilobyte", maximumFractionDigits: 0 });
4usageEl.textContent = `Synced settings: ${fmt.format(report.sync.used / 1024)} of ${fmt.format(report.sync.quota / 1024)}`;

Execution context: the options page. Users rarely think about extension storage, but a short line in options — with a “Clear cached data” button next to it — answers support questions before they are asked and gives users a way out of quota trouble without uninstalling. Intl.NumberFormat with units localises the numbers.

6. Include usage in diagnostics

1export async function diagnostics() {
2  return {
3    version: chrome.runtime.getManifest().version,
4    storage: await usageReport(),
5    largest: await largestKeys(chrome.storage.local, 5),
6  };
7}

Execution context: the service worker, exposed through a “Copy diagnostics” button in options. Key names and sizes are enough to diagnose growth without including any user content. When a user reports lost settings, a diagnostics paste showing sync at 99% resolves the case in minutes.

7. Track usage over time in development

Add a debug page that records usageReport() daily during dogfooding and plots it. Growth that looks harmless over a week — a few kilobytes a day — becomes a quota failure after a year in the field. Catching the slope early is far cheaper than shipping a migration later.

Common mistakes

  • Waiting for quota errors. By the time set fails, the write’s data is gone.
  • Ignoring QUOTA_BYTES_PER_ITEM in sync. A single 9 KB settings object fails even when the area is nearly empty.
  • Counting characters, not bytes. Non-ASCII text uses more bytes than characters.
  • Pruning user data automatically. Prune caches you can rebuild; ask before deleting anything the user created.
  • Unbounded arrays. History, logs and recents grow forever unless capped.

Cross-browser variation

  • Chrome / Edge: getBytesInUse on sync, local and session; quota constants exposed on each area.
  • Firefox: getBytesInUse support has varied by area and version — feature-detect it and fall back to measuring serialised size yourself. Sync quotas match Chrome’s.
  • Safari: limited getBytesInUse support; measure serialised sizes manually and keep sync usage small.

Verification

  1. Run await chrome.storage.sync.getBytesInUse(null) and compare with chrome.storage.sync.QUOTA_BYTES in the worker console.
  2. Write a 9 KB value with safeSyncSet and confirm it throws too-large instead of failing silently.
  3. Fill local past 70% in a test profile, trigger the health alarm, and confirm caches are pruned and a warning is stored.
  4. Confirm the options page shows usage and the clean-up button reduces it.

FAQ

Does getBytesInUse include IndexedDB?

No. It measures only chrome.storage. Use navigator.storage.estimate() for IndexedDB, OPFS and the Cache API.

Is usage shared between Chrome profiles?

No. Each profile has its own storage and quotas; sync quotas apply per synced account.

Can I raise the sync quota?

No. Store large data in local or on your own server and sync only small preferences.

How often should the health check run?

Weekly is plenty for most extensions; storage grows slowly. Run it additionally after migrations and imports, which can add a lot of data at once, and on demand from the options page so a user troubleshooting a problem gets a current number.

Why does usage drop after a browser restart?

chrome.storage.session is cleared when the browser exits, so its usage resets to zero. local and sync usage should be stable across restarts; a drop there means something deleted data.

Other Core APIs & Cross-Browser Data Management Resources