Why You Shouldn’t Use localStorage for Transactions - and What to Use Instead
4th November 2025 • 3 min read — by Aleksandar Trpkovski
When it comes to storing data in the browser, many of us reach for localStorage out of habit. It's simple, it works, and it's supported everywhere. But here's the thing - under the hood, localStorage has some serious limitations, especially when you're dealing with transactions, concurrent updates, and performance-heavy tasks.
Let's explore what those limitations are - and what better alternatives exist for building reliable, performant browser storage.
How localStorage Works
localStorage is a simple key-value storage system that's built into every modern browser. You've probably used it before - it looks something like this:
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme");
Pretty straightforward, right? But here's the catch:
🕒 localStorage operations are synchronous.
That means every setItem, getItem, or removeItem call blocks the main thread until it's done. In small doses, no big deal. But when you start doing large or repeated operations, it can actually freeze your UI and create a poor experience.
Example: Blocking the Main Thread
Want to see this in action? Try running this in your browser console:
console.time("localStorage writes");
for (let i = 0; i < 100000; i++) {
localStorage.setItem(`key-${i}`, `value-${i}`);
}
console.timeEnd("localStorage writes");
alert("Finished writing to localStorage!");
You'll notice your browser freezes for a few seconds before the alert pops up. That's because every setItem call blocks everything else - nothing can run until all those writes finish. Not a great user experience, right? 🙂
A Better Way: IndexedDB
IndexedDB is a built-in browser database that's designed for asynchronous, transactional, and large-scale data storage. It might sound intimidating at first, but it's easier than you think!
Here's how you can do the same thing we just tried, but without freesing the UI:
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("TestDB", 1);
request.onupgradeneeded = () => {
request.result.createObjectStore("store");
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function saveToIndexedDB() {
console.time("IndexedDB writes");
const db = await openDB();
const tx = db.transaction("store", "readwrite");
const store = tx.objectStore("store");
for (let i = 0; i < 10000; i++) {
store.put(`value-${i}`, `key-${i}`);
}
tx.oncomplete = () => {
console.timeEnd("IndexedDB writes");
alert("Finished writing to IndexedDB!");
};
}
saveToIndexedDB();
This version uses asynchronous transactions, which means the browser stays responsive while your data is being written.
Even Easier: localForage
Now, if you're like me and love simplicity, you're going to love localForage. It's a lightweight wrapper around IndexedDB (with fallbacks to WebSQL or localStorage) that gives you a clean, Promise-based API:
npm install localforage
import localforage from "localforage";
async function saveToLocalForage() {
console.time("localForage writes");
const promises = [];
for (let i = 0; i < 10000; i++) {
promises.push(localforage.setItem(`key-${i}`, `value-${i}`));
}
await Promise.all(promises);
console.timeEnd("localForage writes");
alert("Finished writing to localForage!");
}
saveToLocalForage();
This version uses IndexedDB under the hood but gives you the same simple API style as localStorage - without any of the blocking or complexity. Best of both worlds!
Why localStorage Fails for Transactions
When we talk about transactions, we usually mean multiple operations that need to succeed or fail together. Unfortunately, localStorage just isn't built for that. Let me show you why:
| Problem | Description |
|---|---|
| 🧵Synchronous & blocking | Freezes the main thread during heavy writes. |
| ❌No transaction support | You can't rollback or commit multiple changes atomically. |
| ⚔️No concurrency control | Two tabs or scripts can easily overwrite each other's data. |
| 📦Tiny storage limit | Typically 5–10 MB total. |
| 🔓No security | Data is stored in plain text, accessible to any script on the domain. |
In short,
localStorageis perfectly fine for simple preferences or light caching
Unlike localStorage, which saves each item one by one, IndexedDB handles your data in a much safer way. It uses something called transactions, which simply means that all your data changes happen together as a single, reliable action.
If everything goes well, the changes are saved. But if something goes wrong halfway through - say, your browser crashes or one of the operations fails - nothing gets written. Your data stays clean, consistent, and exactly how it was before.
Quick Summary
Here's a user-friendly table I put together to give you a clean and easy way to compare the pros and cons of the three options we discussed above:
| Feature | localStorage | IndexedDB | localForage |
|---|---|---|---|
| Blocking | ✅ Yes | ❌ No | ❌ No |
| Transactions | ❌ No | ✅ Yes | ✅ Yes (via IndexedDB) |
| Concurrency | ❌ Unsafe | ✅ Safe | ✅ Safe |
| Storage Limit | ~5–10 MB | Hundreds of MBs+ | Same as IndexedDB |
| Use Case | Small settings | Complex, large data | Simple async API |
You can find all the working code examples from this article in my GitHub account
Further Reading
Explore more articles that might interest you.