When Should You Use localStorage vs sessionStorage vs IndexedDB?
The Problem
I stored everything in localStorage. User preferences, cached API data, session tokens, temporary form state. Then users started reporting issues:
"My dark mode setting disappeared after I cleared my browser cache.""The app froze when I tried to save a large dataset.""QuotaExceededError: The quota has been exceeded.""My form data leaked to another tab when I opened a new window."The root cause? I was using localStorage for everything, ignoring the differences between browser storage APIs.
Why This Happens
Browser storage APIs serve different purposes. Using the wrong one causes problems:
- localStorage for temp data: Persists across tabs, causes state leakage- sessionStorage for preferences: Lost when tab closes, users hate it- localStorage for large data: Blocks UI thread, causes performance issues- Web Storage for blobs: Exceeds 5MB limit, throws quota errorsExperienced developers on Reddit put it simply:
"localStorage for preferences, IndexedDB for everything else."
"sessionStorage is for data that doesn't need to outlive the tab."
"IndexedDB and OPFS are, in practical terms, unlimited storage."The Solution: Match Storage to Use Case
localStorage - Persistent Simple Data
I use localStorage for small data that must survive browser restarts:
// Good: User preferences that persistlocalStorage.setItem('theme', 'dark');localStorage.setItem('sidebar_collapsed', 'true');localStorage.setItem('preferred_language', 'en');
// Good: Feature flagslocalStorage.setItem('beta_features_enabled', 'true');
// Good: Small cached datalocalStorage.setItem('last_visit', new Date().toISOString());Characteristics:
- Limit: ~5 MB per origin
- Persistence: Survives browser close/restart
- API: Synchronous (blocks main thread)
- Scope: Shared across all tabs/windows of same origin
- Data types: Strings only (must serialize objects)
sessionStorage - Temporary Tab-Scoped Data
I use sessionStorage for data that only matters during a single tab session:
// Good: Multi-step form wizard statesessionStorage.setItem('wizard_step', '2');sessionStorage.setItem('wizard_data', JSON.stringify({ name: 'John',}));
// Good: Temporary filters that reset on tab closesessionStorage.setItem('temp_filter', 'active');
// Good: One-time redirect tokenssessionStorage.setItem('redirect_after_login', '/dashboard');Characteristics:
- Limit: ~5 MB per origin
- Persistence: Cleared when tab/window closes
- API: Synchronous (blocks main thread)
- Scope: Isolated per tab/window
- Data types: Strings only (must serialize objects)
The key difference from localStorage: each tab gets its own sessionStorage. Data in one tab cannot leak to another.
IndexedDB - Large and Structured Data
I use IndexedDB for everything else:
// Good: Offline data storageconst dbRequest = indexedDB.open('MyAppDB', 1);
dbRequest.onupgradeneeded = (event) => { const db = event.target.result;
// Create object store for cached articles const articleStore = db.createObjectStore('articles', { keyPath: 'id' }); articleStore.createIndex('category', 'category', { unique: false });
// Create object store for user files db.createObjectStore('files', { keyPath: 'id' });};
// Good: Storing blobs and filesfunction saveFile(file) { const db = await openDB(); const tx = db.transaction('files', 'readwrite'); const store = tx.objectStore('files'); store.put({ id: file.name, blob: file, uploadedAt: new Date() });}
// Good: Large datasets with queryingasync function getArticlesByCategory(category) { const db = await openDB(); const tx = db.transaction('articles', 'readonly'); const store = tx.objectStore('articles'); const index = store.index('category'); return index.getAll(category);}Characteristics:
- Limit: Virtually unlimited (often 50% of available disk)
- Persistence: Survives browser close/restart
- API: Asynchronous (non-blocking)
- Scope: Shared across all tabs/windows of same origin
- Data types: Any structured data, including blobs and files
- Query support: Yes (indexes)
Decision Matrix
When I need to choose, I use this table:
┌─────────────────────────────┬──────────────┬───────────────┬────────────┐│ Criteria │ localStorage │ sessionStorage│ IndexedDB │├─────────────────────────────┼──────────────┼───────────────┼────────────┤│ Data persists across │ Yes │ No │ Yes ││ browser sessions │ │ │ │├─────────────────────────────┼──────────────┼───────────────┼────────────┤│ Data isolated per tab │ No │ Yes │ No │├─────────────────────────────┼──────────────┼───────────────┼────────────┤│ Max storage size │ ~5 MB │ ~5 MB │ ~Unlimited │├─────────────────────────────┼──────────────┼───────────────┼────────────┤│ API type │ Synchronous │ Synchronous │ Async │├─────────────────────────────┼──────────────┼───────────────┼────────────┤│ Data types │ Strings only │ Strings only │ Any + Blobs│├─────────────────────────────┼──────────────┼───────────────┼────────────┤│ Query/index support │ No │ No │ Yes │├─────────────────────────────┼──────────────┼───────────────┼────────────┤│ Performance impact │ Can block UI │ Can block UI │ Non-blocking│└─────────────────────────────┴──────────────┴───────────────┴────────────┘Quick Decision Flow
I follow this flow when choosing:
1. Does data need to persist across browser sessions? NO → sessionStorage YES → Go to step 2
2. Is the data under 5MB and simple key-value? YES → localStorage NO → IndexedDB
3. Do I need to query or index the data? YES → IndexedDB NO → localStorage (if small) or IndexedDB (if large)
4. Is the data a file or blob? YES → IndexedDB NO → Continue to step 5
5. Will blocking the UI cause issues? YES → IndexedDB (async) NO → localStorage (if small enough)Real-World Examples
Example 1: Theme Preferences
// CORRECT: localStorage for persistent preferencesfunction saveTheme(theme) { localStorage.setItem('theme', theme);}
function loadTheme() { return localStorage.getItem('theme') || 'light';}Example 2: Multi-Step Form Wizard
// CORRECT: sessionStorage for temporary wizard statefunction saveWizardStep(step, data) { sessionStorage.setItem('wizard_step', step); sessionStorage.setItem('wizard_data', JSON.stringify(data));}
function loadWizardState() { const step = sessionStorage.getItem('wizard_step'); const data = JSON.parse(sessionStorage.getItem('wizard_data') || '{}'); return { step, data };}
// Clear on completionfunction completeWizard() { sessionStorage.removeItem('wizard_step'); sessionStorage.removeItem('wizard_data');}Example 3: Offline Article Cache
// CORRECT: IndexedDB for large structured dataasync function cacheArticles(articles) { const db = await openDB(); const tx = db.transaction('articles', 'readwrite'); const store = tx.objectStore('articles');
for (const article of articles) { store.put(article); }
await tx.done;}
async function getCachedArticles(category) { const db = await openDB(); const tx = db.transaction('articles', 'readonly'); const store = tx.objectStore('articles'); const index = store.index('category'); return index.getAll(category);}Example 4: What NOT to Do
// WRONG: Large data in localStorage (blocks UI)const hugeData = fetchLargeDataset();localStorage.setItem('cache', JSON.stringify(hugeData)); // Blocks main thread!
// WRONG: Session tokens in localStorage (security risk)localStorage.setItem('auth_token', token); // Use httpOnly cookies instead!
// WRONG: Temporary filters in localStorage (pollutes storage)localStorage.setItem('temp_filter', filter); // Should use sessionStorage
// WRONG: Blob in localStorage (will hit quota)const blob = await response.blob();localStorage.setItem('file', blob); // Can't store blobs, and too large anywaySecurity Warning
I never store sensitive data in any client-side storage:
// NEVER do this:localStorage.setItem('password', password);localStorage.setItem('api_key', apiKey);localStorage.setItem('auth_token', token);sessionStorage.setItem('credit_card', cardNumber);indexedDB.put('secrets', { ssn: '123-45-6789' });
// INSTEAD: Use httpOnly cookies for auth// Let the server handle sensitive dataClient-side storage is accessible to any JavaScript running on the page. XSS attacks can steal anything stored in localStorage, sessionStorage, or IndexedDB.
Summary
I choose browser storage based on three factors: persistence, data size, and query needs.
| Storage | Use Case |
|---|---|
| localStorage | Small, persistent, simple key-value data (preferences, settings) |
| sessionStorage | Small, temporary, tab-scoped data (form state, temporary filters) |
| IndexedDB | Large, structured, queryable data (offline storage, cached content, files) |
The golden rule: localStorage for preferences, IndexedDB for everything else.
And never store sensitive data in client-side storage. Use httpOnly cookies for authentication and keep sensitive processing server-side.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments