iOS Safari Limitations
Understand iOS Safari's PWA constraints — storage limits, eviction, service worker lifecycle, and workarounds.
Safari on iOS supports Service Workers, Cache API, and IndexedDB — but with significant constraints that differ from desktop browsers. Understanding these is essential for production offline-first apps.
7-Day Eviction Policy
The most critical limitation: iOS Safari evicts all website data — IndexedDB, Cache API, and Service Worker registrations — after 7 days of no user engagement.
| Scenario | Data Persistence |
|---|---|
| User visits weekly | Data survives (engagement resets 7-day timer) |
| User installs to Home Screen | Data persists indefinitely |
| User visits once, never returns | Data evicted after 7 days |
| User visits via installed PWA | Data persists (standalone exception) |
| User clears Safari data | Immediate eviction (user-initiated) |
Mitigation
Encourage PWA installation — standalone mode is the only reliable protection. Detect eviction on startup:
async function detectEviction() {
try {
const db = await openDB();
const tx = db.transaction("meta", "readonly");
const store = tx.objectStore("meta");
const marker = await new Promise((r) => {
const req = store.get("app-initialized");
req.onsuccess = () => r(req.result);
});
return !marker;
} catch {
return true;
}
}IndexedDB Storage Limit
iOS Safari imposes a ~50 MB soft limit on IndexedDB — significantly lower than desktop browsers (typically unlimited up to free disk space). Above 50 MB, QuotaExceededError is thrown on writes.
Monitoring
async function monitorStorage() {
const estimate = await navigator.storage.estimate();
const usageMB = estimate.usage / (1024 * 1024);
const quotaMB = estimate.quota / (1024 * 1024);
if (usageMB > 40) console.warn("Approaching iOS IndexedDB limit");
return { usageMB, quotaMB };
}Note: navigator.storage.estimate() on iOS often reports a very large quota (~500 MB+) but writes will still fail around 50 MB. Trim cached data aggressively and implement proactive cleanup.
No beforeinstallprompt
iOS Safari does not fire the beforeinstallprompt event. Use the Share menu "Add to Home Screen" flow instead:
function showInstallGuide() {
if (window.navigator.standalone) return;
const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent);
if (isIOS && "serviceWorker" in navigator) {
showIOSInstallInstructions(); // 1. Share → 2. Add to Home Screen
}
}Detect Standalone Mode
const standalone = {
ios: window.navigator.standalone,
standard: window.matchMedia("(display-mode: standalone)").matches,
};WKWebView Constraints
Third-party browsers on iOS (Chrome, Firefox, Edge) are WKWebView wrappers around Safari's engine. They share Safari's limitations:
| Constraint | Detail |
|---|---|
| Service Worker support | Available from iOS 16.4+ in WKWebView. Before 16.4, no SW support. |
| Storage sharing | Each browser has its own storage sandbox |
| IndexedDB limits | Same 50 MB soft limit |
| Eviction policy | Same 7-day eviction (no standalone exception for most) |
Push Notifications
| iOS Version | Push Support |
|---|---|
| < 16.4 | Not supported |
| 16.4+ | Supported (requires standalone mode + APNs entitlement) |
Push notifications on iOS can only be tested on a physical device, not the simulator.
Private Browsing Mode
In Safari private browsing, IndexedDB and Cache API may throw errors or return empty databases. Data does not persist after the session ends.
async function isPrivateMode() {
try {
const db = indexedDB.open("__detect_private__");
return new Promise((resolve) => {
db.onerror = () => resolve(true);
db.onsuccess = () => {
indexedDB.deleteDatabase("__detect_private__");
resolve(false);
};
});
} catch {
return true;
}
}Service Worker Lifecycle on iOS
iOS terminates service workers more aggressively than desktop:
| Behavior | Desktop (Chrome) | iOS Safari |
|---|---|---|
| SW idle timeout | ~30 seconds | ~5-10 seconds |
| SW wakes on fetch | Yes | Yes |
| SW wakes on push | Yes | Yes (iOS 16.4+) |
| SW wakes on sync event | Yes | N/A (no Background Sync) |
Extension via waitUntil | Full operation completes | May be interrupted earlier |
Implications: Background sync does not exist on iOS — use the online event listener instead. Mutation queue processing must happen in the main thread, not the SW.
navigator.storage.persist() on iOS
| Query | Behavior |
|---|---|
navigator.storage.persist() | Returns false (denied) |
navigator.storage.persisted() | Returns false |
| Standalone PWA exception | Data is not evicted even without persistent storage |
On iOS, persistent storage requests are always denied. The only reliable prevention is standalone mode (Home Screen installation).
Compatibility Summary
| Feature | Desktop Chrome | iOS Safari | iOS Standalone PWA |
|---|---|---|---|
| Service Worker | Full | Supported | Supported |
| Cache API | Full | Supported | Supported |
| IndexedDB | Unlimited | ~50 MB limit | ~50 MB limit |
| 7-Day Eviction | No | Yes | No (exempt) |
| Background Sync | Supported | Not supported | Not supported |
| Push Notifications | Full | iOS 16.4+ only | iOS 16.4+ |
| beforeinstallprompt | Supported | Not supported | N/A |
| Persistent Storage | Supported | Denied | N/A |
| Private Browsing IDB | Works | May fail | N/A |
| SW Idle Timeout | ~30s | ~5-10s | ~5-10s |
Testing on iOS
| Method | Limitation |
|---|---|
| Safari on real device | Best option — reflects actual behavior |
| iOS Simulator (Xcode) | Safari works, but no push, no standalone mode simulation |
| Remote debugging | Safari → Develop → [Device Name] → Web Inspector |
| BrowserStack / Sauce Labs | Good for quick checks, limited SW debugging |
Next Steps
- Storage Quota Management — quota limits and eviction
- Offline-First Architecture — build resilient for all platforms
Swoff