In modern web development, we often treat localStorage as a bottomless bucket for small bits of state—session tokens, UI preferences, or cart data. But as I recently discovered while scaling our international DTC infrastructure, that bucket has a very hard, very unforgiving ceiling.
The Symptom: QuotaExceededError
During a routine audit of our tracking stack, I hit the dreaded QuotaExceededError. This error occurs when a website attempts to save more data than the browser allows for that specific domain. Most browsers (Chrome, Safari, Edge) cap this at roughly 5MB to 10MB.
While investigating, I found the culprit wasn't our own code, but Osano—our consent management platform—which was running in Discovery Mode.
The Learning: When Auditing Becomes Bloating
In "Discovery Mode," compliance tools like Osano act as a "tattle-tale." Every time a new script, cookie, or network request is detected, it logs it to a local storage key (in this case, osano_consentmanager_tattles) to build an audit list.
For a complex site with multiple integrations—like our Klaviyo migration, Rollbar error tracking, and Shopify BI connectors—this list grows exponentially. In our case, this single "audit" key ballooned to over 9MB, effectively hijacking the entire storage quota and blocking every other critical script from functioning.
The Diagnostic Tool
To find the "smoking gun," I used a script to visualize exactly how our 5MB-10MB was being spent. If you're seeing storage errors, run this in your console to see a ranked list of your data-hogs:
/**
* LocalStorage Usage Audit
* Calculates size in KB (assuming UTF-16) and sorts by size.
*/
(function() {
const data = [];
let totalSize = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
// localStorage uses UTF-16, so each character is 2 bytes
const sizeInBytes = (key.length + value.length) * 2;
const sizeInKB = sizeInBytes / 1024;
data.push({
"Item Name": key,
"Size (KB)": parseFloat(sizeInKB.toFixed(2))
});
totalSize += sizeInKB;
}
// Sort descending by size to find the culprit
data.sort((a, b) => b["Size (KB)"] - a["Size (KB)"]);
console.table(data);
console.log(`Total Usage: ${(totalSize / 1024).toFixed(2)} MB / 10.00 MB`);
if (totalSize > 4500) {
console.warn("CRITICAL: You are nearing the browser-imposed 5MB-10MB limit.");
}
})();
Limits by browser
| Browser | Local Storage Limit | Total Storage (IndexedDB/Cache) | Notes |
|---|---|---|---|
| Chrome | ~5MB - 10MB | Up to 80% of disk space | Shared pool approach; mirrors Edge. |
| Firefox | ~5MB - 10MB | Up to 50% of free disk space | Generous with IndexedDB; prompts for large data. |
| Safari | ~5MB | Varies based on disk space | Most restrictive; aggressive with data purging. |
| Edge | ~5MB - 10MB | Up to 80% of disk space | Built on Chromium; same limits as Chrome. |
The Three Keys to Better Storage Management
- Avoid Discovery Mode in Production: Tools meant for auditing should be used in staging. Once you have your baseline of trackers and cookies, move to "Managed" or "Permissive" modes to stop the storage bloat.
- Monitor Your "UTF-16 Tax": Remember that localStorage stores strings in UTF-16 format. This means 1MB of raw text actually takes up 2MB of your quota.
- Fail Gracefully: Always wrap your localStorage.setItem() calls in a try...catch block. If the storage is full, your app should continue to work, even if it means losing non-essential cached data.
Final Thoughts
As we continue to modernize our core technical foundation, it’s a reminder that even "passive" compliance tools have a footprint. Keeping a lean localStorage isn't just about clean code—it's about ensuring your site has the "breathing room" it needs to scale globally.