How to check localstorage size

In the Console, add up key.length + localStorage.getItem(key).length over every key, then double it for bytes, because Chrome counts UTF-16. The demo store came to 120045 characters, 240090 bytes. The ceiling measured on this build is 10 MiB per origin, and the write that crosses it throws QuotaExceededError.

Why check this

Storage fills up in production, not in a test. A store grows one release at a time: a cached catalogue, a list of dismissed banners, a draft nobody deletes. The write that crosses the line throws, and because most code writes without a try, the exception lands as an unhandled error and the feature that was mid-write loses its data. Measure before a release that adds a cache, and on a profile that has been in use for a year.

The second reason is arithmetic. Two figures are easy to reach and both are wrong. JSON.stringify(localStorage).length counts JSON punctuation and escaping, and navigator.storage.estimate() reports a number that has nothing to do with this store. Steps 4 and 5 measure both against the ceiling from step 6.

Prerequisites

Steps

  1. Step 1.

    Start the demo page and open http://localhost:8731/ in Chrome.

    node storage-demo.mjs
    
    serving http://localhost:8731/ and http://localhost:8732/
  2. Step 2.

    Measure what the origin holds. The first line writes a 120000-character draft, so there is something to measure.

    localStorage.setItem('draft', 'x'.repeat(120000));
    let chars = 0;
    for (let i = 0; i < localStorage.length; i++) {
      const k = localStorage.key(i);
      chars += k.length + localStorage.getItem(k).length;
    }
    `${chars} characters, ${chars * 2} bytes, ${(chars * 2 / 10485760 * 100).toFixed(2)}% of 10 MiB`
    
    120045 characters, 240090 bytes, 2.29% of 10 MiB
  3. Step 3.

    Find which key is spending it.

    Object.keys(localStorage)
      .map((k) => [k, k.length + localStorage.getItem(k).length])
      .sort((a, b) => b[1] - a[1])
      .map(([k, n]) => `${String(n).padStart(8)}  ${k}`)
      .join('\n')
    
      120005  draft
        31  cart
         9  theme
  4. Step 4.

    Compare the sum with the figure most snippets print. Store a value holding a quote and a backslash first.

    localStorage.setItem('quote', 'he said "ok"' + String.fromCharCode(92) + 'done');
    let chars = 0;
    for (let i = 0; i < localStorage.length; i++) {
      const k = localStorage.key(i);
      chars += k.length + localStorage.getItem(k).length;
    }
    const j = JSON.stringify(localStorage);
    [
      'getItem("quote")                     = ' + localStorage.getItem('quote'),
      'the same value inside the JSON       = ' + j.slice(j.indexOf('"quote"'), j.indexOf('"quote"') + 32),
      'key + value characters               = ' + chars,
      'JSON.stringify(localStorage).length  = ' + j.length,
    ].join('\n')
    
    getItem("quote")                     = he said "ok"\done
    the same value inside the JSON       = "quote":"he said \"ok\"\\done","
    key + value characters               = 120067
    JSON.stringify(localStorage).length  = 120101

    The stored value is 17 characters and takes 20 inside the JSON: each quote and the backslash become two. The rest of the difference is the braces, colons and commas JSON adds around every pair.

  5. Step 5.

    Ask the Storage API how much room the origin has.

    navigator.storage.estimate().then((e) => console.log(JSON.stringify({ usage: e.usage, quota: e.quota, quotaMiB: Math.round(e.quota / 1048576) }, null, 2)))
    
    {
    "usage": 0,
    "quota": 10737418240,
    "quotaMiB": 10240
    }

    Usage is zero on an origin holding 120 kB, and the quota is 10240 MiB. Keep both for step 7.

  6. Step 6.

    Fill the store until a write fails. The loop starts at 65536 characters and ends at one, so it stops against the real edge, not a chunk boundary.

    localStorage.clear();
    let i = 0, chars = 0, err = null;
    for (const size of [65536, 1024, 16, 1]) {
      for (;;) {
        const k = 'k' + String(i).padStart(6, '0');
        try { localStorage.setItem(k, 'x'.repeat(size)); } catch (e) { err = e; break; }
        i++; chars += size + k.length;
      }
    }
    JSON.stringify({ keys: i, characters: chars, bytes: chars * 2, MiB: +(chars * 2 / 1048576).toFixed(4), name: err.name, code: err.code, message: err.message }, null, 2)
    
    {
    "keys": 143,
    "characters": 5242873,
    "bytes": 10485746,
    "MiB": 10,
    "name": "QuotaExceededError",
    "code": 22,
    "message": "Failed to execute 'setItem' on 'Storage': Setting the value of 'k000143' exceeded the quota."
    }

    5242873 characters fitted, in key names and values together. Ten MiB is 5242880 characters at two bytes each, so the store stopped 7 characters short of the round figure.

  7. Step 7.

    Ask the Storage API again, with the store full.

    navigator.storage.estimate().then((e) => console.log(JSON.stringify({ usage: e.usage, quota: e.quota, quotaMiB: Math.round(e.quota / 1048576) }, null, 2)))
    
    {
    "usage": 0,
    "quota": 10737418240,
    "quotaMiB": 10240
    }

    Identical to step 5. Ten MiB of local storage moved the usage by nothing, and the quota is 1024 times the limit that threw.

  8. Step 8.

    Read what the failed write left behind, and prove the seven free characters are real.

    JSON.stringify({
      'localStorage.length': localStorage.length,
      'getItem on the key that threw': localStorage.getItem('k000143'),
      'getItem on the key before it': localStorage.getItem('k000142').length + ' characters',
      'a 2-character write after the failure': (() => {
        try { localStorage.setItem('tiny', 'ok'); return 'stored: ' + localStorage.getItem('tiny'); } catch (e) { return e.name; }
      })(),
    }, null, 2)
    
    {
    "localStorage.length": 143,
    "getItem on the key that threw": null,
    "getItem on the key before it": "16 characters",
    "a 2-character write after the failure": "stored: ok"
    }

    The write that threw stored nothing and every earlier key is intact. Then tiny plus ok, six characters, went in where k000143 plus one character, eight characters, had failed. Key names are charged to the quota like values.

  9. Step 9.

    Measure the largest single value, for three kinds of character.

    const maxFor = (ch) => {
      let lo = 0, hi = 6000000;
      while (lo < hi) {
        const mid = Math.ceil((lo + hi) / 2);
        localStorage.clear();
        try { localStorage.setItem('a', ch.repeat(mid)); lo = mid; } catch { hi = mid - 1; }
      }
      localStorage.clear();
      return lo;
    };
    JSON.stringify({ ascii: maxFor('x'), cyrillic: maxFor('ы'), emoji: maxFor('\u{1F600}') }, null, 2)
    
    {
    "ascii": 5242879,
    "cyrillic": 5242879,
    "emoji": 2621439
    }

    The ascii and the Cyrillic values are the same length, so the cost is per code unit and not per UTF-8 byte. The emoji count is half, because that character is two code units. Add the one-character key name to 5242879 and the total is 5242880 characters, 10485760 bytes, 10 MiB exactly.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | A total under 1 MiB | Normal for settings and flags | Record it, repeat after the release that adds a cache. | | One key holding most of the total | A document or a list is parked in storage | Move it to IndexedDB or the Cache API, which are not capped at 10 MiB. | | JSON.stringify(localStorage).length above the summed length | Escaping and JSON punctuation, not stored data | Use the sum from step 2 as the number you report. | | estimate() reporting a quota in the thousands of MiB | That quota covers IndexedDB and the Cache API | Ignore it for this store. The local storage limit is the one step 6 measured. | | QuotaExceededError, code 22 | The origin is at the ceiling | Remove keys, or move the payload. Wrap the write in try so the feature degrades. | | The key that threw is absent, earlier keys intact | One setItem is all or nothing | Retry with a smaller value. Nothing is half written. |

Thresholds

10 MiB per origin: 5242880 UTF-16 code units, counting key names and values together Source: Measured three ways on Chrome 152.0.7977.76, 2026-09-11, see the Verified block
Largest single entry: a 5242879-character value under a one-character key, 10485760 bytes with the key Source: Binary search in step 9, same build and date
The same 5242873 characters fitted when the session store was filled by the same loop Source: Measured on Chrome 152.0.7977.76, 2026-09-11

Common mistakes

Sign: A size report built from JSON.stringify(localStorage).length, which disagrees with the sum over the keys.Cause: That expression serialises the store first. Every quote and backslash in a value becomes two characters, and JSON adds braces, quotes, colons and commas around every pair. In step 4 it read 120101 against 120067 characters actually stored, on a store with one escaped value.
Sign: navigator.storage.estimate() says gigabytes are free and the next setItem still throws.Cause: estimate() reports the quota-managed bucket, which covers IndexedDB, the Cache API and service worker registrations. Local storage is outside it. In steps 5 and 7 usage stayed at 0 with 10 MiB of keys in the store, and the quota it reported was 1024 times the limit that threw.
Sign: A budget built from value lengths alone runs out earlier than expected.Cause: Key names are charged to the quota. Step 8 shows the accounting: a 4-character key with a 2-character value was accepted in the same store where a 7-character key with a 1-character value had been rejected a moment before.
Sign: A limit calculated in UTF-8 bytes, which passes the test and fails for users writing in a non-Latin script.Cause: Chrome counts UTF-16 code units, two bytes each, whatever the character. Step 9 stored the same number of Cyrillic characters as ascii ones, and half as many emoji, because an emoji is two code units.

What to check next

FAQ

What is the max size of localstorage?

On the build measured here, 10 MiB per origin, or 5242880 UTF-16 code units counting key names and values. The HTML standard sets no number, so another browser can differ. Measure with step 6 rather than quoting a figure.

How to reproduce quotaexceedederror in localstorage?

Step 6 is the reproduction. Writing 65536-character values in a loop reaches the ceiling in 80 iterations and raises a DOMException named QuotaExceededError, with code 22 and a message naming the key that failed. Clear the store afterwards.

Does session storage have the same limit?

Yes, and it is a separate allowance. The same fill loop against sessionStorage on this build stopped at 5242873 characters, the figure local storage reached in step 6. Filling one does not reduce the other.

What should I do when a store is near the limit?

Move the largest key out. IndexedDB and the Cache API fall under the quota estimate() reports, 10240 MiB here, and both are asynchronous, so a large write does not block the main thread the way a multi-MiB setItem does.

Verified

Verified by Maks VernyChrome 152.0.7977.76node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

intermediate8 minpublished updated Maks Verny