Choosing Client-Side Storage in Safari: LocalStorage vs IndexedDB
A decision guide for storing data in Safari. Compare LocalStorage and IndexedDB for quotas, data types, and Safari's tracking protections, then validate your choice in Web Inspector.
17 Nov 2025, 09:24 UTC

The decision
Deciding where to store data in Safari is a trade-off between API simplicity and data resilience. LocalStorage offers a synchronous key-value string API that is trivial to implement, but it blocks the main UI thread during reads and writes. IndexedDB provides an asynchronous, transactional object store that handles complex data and larger volumes, at the cost of more verbose code. In Safari, neither option guarantees permanent persistence: Intelligent Tracking Prevention (ITP) may purge client-side storage for sites a user has not visited recently, and Private Browsing mode does not guarantee storage survives the session. The practical takeaway: choose LocalStorage only for small, simple, non-critical string data; choose IndexedDB for structured or larger datasets, and design your application to tolerate eviction in both cases.
Constraints that change the answer in Safari
Before comparing APIs, account for three Safari-specific constraints. First, ITP may remove script-writable storage for domains classified as trackers or for sites the user has not interacted with for an extended period; community documentation often cites a seven-day inactivity threshold, but treat this as a risk factor rather than a fixed rule. Second, Private Browsing windows are even less predictable: data may be discarded when the last private window closes. Third, mobile Safari devices have less memory headroom, so synchronous I/O on the main thread is more likely to cause visible jank than on desktop.
LocalStorage vs IndexedDB at a glance
| Factor | LocalStorage | IndexedDB |
|---|---|---|
| Data model | String key-value pairs only | Structured objects, Blobs, ArrayBuffers |
| API style | Synchronous, blocking | Asynchronous, transactional |
| Typical size guidance | Small; historically around 5 MB per origin | Larger datasets; quota depends on available storage |
| Main-thread impact | Blocks UI during I/O | Non-blocking for reads/writes |
| Query capability | Key lookup only | Indexes and cursors for search |
| Safari persistence risk | Subject to ITP eviction and Private Browsing limits | Subject to ITP eviction and Private Browsing limits |
Trade-offs
Simplicity and data shape
LocalStorage wins for a handful of user preferences or feature flags. Converting objects to JSON strings adds serialization overhead and loses native Blob storage. IndexedDB stores objects directly and supports indexes, but every operation goes through request and transaction callbacks, which increases code volume and error-handling paths.
Performance on the main thread
Because LocalStorage is synchronous, a large write or a high-frequency update loop can cause visible jank, especially on mobile Safari. IndexedDB runs I/O off the main thread, so the UI remains responsive, though you must handle asynchronous completion states and transaction lifecycle events.
Persistence expectations
If your feature depends on durable storage across weeks of inactivity, neither API alone is sufficient in Safari. You need a server-side or re-fetch strategy that restores state when local storage is missing. Treat any client-side data as a cache, not a source of truth.
A concrete validation pattern
Before committing to an implementation, validate quota and behavior in Safari's developer tools. Enable the Develop menu by opening Safari > Settings > Advanced and checking Show Develop menu in menu bar. Then choose Develop > Show Web Inspector (or press Option+Command+I) and select the Storage tab to inspect LocalStorage keys, IndexedDB databases, and their sizes.
Run the following in the Web Inspector Console to check the origin's estimated quota and current usage. This is a diagnostic, not a guaranteed per-API breakdown.
// Check estimated storage quota and usage for the current origin.
if (navigator.storage && navigator.storage.estimate) {
navigator.storage.estimate().then((estimate) => {
console.log('Quota:', estimate.quota);
console.log('Usage:', estimate.usage);
});
} else {
console.log('Storage estimation API not available in this Safari version.');
}Expected check: the console prints quota and usage numbers. If the API is unavailable, fall back to the Storage tab. Risk: the estimate reflects the origin's total quota, not a per-API breakdown, and values may differ across Safari versions.
Use this minimal IndexedDB open pattern to verify the database layer is reachable. Replace my-db and my-store with your own names.
// Replace 'my-db' and 1 with your database name and version.
const request = indexedDB.open('my-db', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('my-store')) {
db.createObjectStore('my-store', { keyPath: 'id' });
}
};
request.onsuccess = (event) => {
console.log('IndexedDB opened successfully:', event.target.result);
event.target.result.close();
};
request.onerror = (event) => {
console.error('IndexedDB open failed:', event.target.error);
};Expected check: the success callback logs the database object. If you see an error, check for private browsing restrictions or quota exceptions. Risk: in Private Browsing, IndexedDB may be unavailable or ephemeral; always handle the error path rather than assuming success.
Limitations and how to check the result
- Exact LocalStorage quotas vary by Safari version, operating system, and available disk space; the 5 MB figure is a historical guideline, not a hard limit.
- ITP thresholds and Private Browsing behavior can change between Safari releases, so re-test on your target versions.
- IndexedDB transactions auto-commit when the event loop yields; long-running transactions may abort if control returns to the browser before completion.
- Storage APIs do not replace server-side persistence for critical data.
To verify the outcome, open the Storage tab in Safari Web Inspector after running your code to confirm keys or object stores exist. Reload the page and read back a known key to confirm persistence within the same session. Test in a Private Browsing window to observe whether data survives closing and reopening the window. If your deployment targets users who return infrequently, assume ITP may evict storage and design a re-initialization path.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.