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
- Chrome with DevTools, open on the origin you are measuring. Storage is per origin: scheme, host and port all count.
- Node 22 and the demo page from How to check localstorage in chrome, which serves
http://localhost:8731/. - Every number below is one capture on one machine: Chrome 152.0.7977.76 on Windows 11, 2026-09-11. The ceiling belongs to the browser build, not to the platform. The HTML standard section on web storage sets no size and leaves the limit to the browser.
- Steps 6 to 9 fill and clear the store. Run them on the demo page or a throwaway profile, never on one holding work you want to keep.
Steps
- Step 1.
Start the demo page and open
http://localhost:8731/in Chrome.node storage-demo.mjsserving http://localhost:8731/ and http://localhost:8732/ - 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 - 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 - 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 = 120101The 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.
- 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.
- 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.
- 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.
- 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
tinyplusok, six characters, went in wherek000143plus one character, eight characters, had failed. Key names are charged to the quota like values. - 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
Common mistakes
What to check next
- How to check localstorage in chrome: read the keys before measuring them.
- How to check session storage in chrome: the per-tab store, with its own copy of the quota.
- How to check what a service worker has cached: where a payload that outgrew this store belongs.
- How to check console errors on a website: an uncaught quota error surfaces there.
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.
Related on this site
intermediate8 minpublished updated Maks Verny