Skip to content

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:

User Reports
"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:

Common Issues from Wrong Storage Choice
- 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 errors

Experienced developers on Reddit put it simply:

Reddit Consensus
"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:

localStorage-examples.js
// Good: User preferences that persist
localStorage.setItem('theme', 'dark');
localStorage.setItem('sidebar_collapsed', 'true');
localStorage.setItem('preferred_language', 'en');
// Good: Feature flags
localStorage.setItem('beta_features_enabled', 'true');
// Good: Small cached data
localStorage.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:

sessionStorage-examples.js
// Good: Multi-step form wizard state
sessionStorage.setItem('wizard_step', '2');
sessionStorage.setItem('wizard_data', JSON.stringify({
name: 'John',
}));
// Good: Temporary filters that reset on tab close
sessionStorage.setItem('temp_filter', 'active');
// Good: One-time redirect tokens
sessionStorage.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:

indexeddb-examples.js
// Good: Offline data storage
const 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 files
function 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 querying
async 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:

Storage Decision Matrix
┌─────────────────────────────┬──────────────┬───────────────┬────────────┐
│ 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:

Decision Flow
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

theme-storage.js
// CORRECT: localStorage for persistent preferences
function saveTheme(theme) {
localStorage.setItem('theme', theme);
}
function loadTheme() {
return localStorage.getItem('theme') || 'light';
}

Example 2: Multi-Step Form Wizard

wizard-storage.js
// CORRECT: sessionStorage for temporary wizard state
function 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 completion
function completeWizard() {
sessionStorage.removeItem('wizard_step');
sessionStorage.removeItem('wizard_data');
}

Example 3: Offline Article Cache

article-cache.js
// CORRECT: IndexedDB for large structured data
async 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

bad-examples.js
// 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 anyway

Security Warning

I never store sensitive data in any client-side storage:

security-examples.js
// 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 data

Client-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.

StorageUse Case
localStorageSmall, persistent, simple key-value data (preferences, settings)
sessionStorageSmall, temporary, tab-scoped data (form state, temporary filters)
IndexedDBLarge, 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