Designing Offline Sync with the Background Sync API
Building resilient web applications requires maintaining seamless functionality even when network connectivity drops. This article explores how engineering teams implement reliable offline data synchronization using the Background Sync API, covering client-side storage architecture with IndexedDB, service worker integration, request queue management, conflict resolution strategies, and fallback mechanisms for unsupported platforms.
The Core Architecture: Service Workers and IndexedDB
The Background Sync API defers actions until the user has stable connectivity. Because standard HTTP requests fail immediately when offline, development teams use a three-tiered architecture to handle offline actions:
- User Interface and Local Storage: When a user submits an action (e.g., sending a message or updating a profile) while offline, the application intercepts the request and saves the payload into IndexedDB.
- Sync Registration: The application requests a sync
event from the Service Worker registration using
registration.sync.register('sync-tag-name'). - Service Worker Execution: The browser wakes the
service worker as soon as connectivity is detected—even if the user has
navigated away or closed the tab—and fires the
syncevent to process the stored queue.
// Registering sync in the main application thread
async function queueOfflineAction(data) {
await saveToIndexedDB('outbox', data);
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-outbox');
} else {
// Fallback for browsers without Background Sync support
window.addEventListener('online', () => processQueueDirectly());
}
}Processing Requests in the Service Worker
Inside the service worker, the team listens for the sync
event. Resiliency depends on chaining the synchronization logic to the
event.waitUntil() promise. If the promise rejects, the
browser automatically schedules a retry according to its internal
backoff algorithm.
// Handling the sync event in the service worker
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-outbox') {
event.waitUntil(flushOutbox());
}
});
async function flushOutbox() {
const pendingRequests = await getPendingRequestsFromIndexedDB('outbox');
for (const item of pendingRequests) {
try {
const response = await fetch(item.url, {
method: item.method,
headers: item.headers,
body: JSON.stringify(item.body)
});
if (response.ok) {
await deleteFromIndexedDB('outbox', item.id);
} else if (response.status >= 400 && response.status < 500) {
// Client errors (e.g., 400 Bad Request) won't fix themselves; discard or move to dead-letter queue
await moveToDeadLetterQueue(item, response.status);
} else {
// Server errors (5xx) trigger retry by throwing
throw new Error('Server returned error status');
}
} catch (error) {
// Propagate error to let the browser know the sync failed and needs retrying
throw error;
}
}
}Critical Patterns for Resilient Synchronization
Teams must address specific distributed systems challenges to ensure data integrity during delayed transmission:
1. Idempotency Keys
Network requests can succeed on the server while the network fails before the client receives the acknowledgment. To prevent duplicate records (such as duplicate payments or multiple comments), teams attach a unique UUID (idempotency key) to each offline mutation. The server tracks these keys and returns the cached result if a duplicate request arrives.
2. Differentiating Errors and Dead-Letter Queues
Not all failures should trigger an automatic retry. Transient network dropouts and 5xx server errors warrant retries. However, 4xx responses (such as validation failures or permission revocations) will repeatedly fail. Teams route permanent failures to a dead-letter queue in IndexedDB and surface them to the user via notifications once online.
3. Conflict Resolution
When offline modifications conflict with server-side updates made elsewhere, teams implement clear resolution patterns:
- Last-Write-Wins (LWW): Updates include a UTC timestamp, and the latest timestamp overwrites older states. This is simple but can cause silent data loss.
- Operational Transformation / CRDTs: Useful for collaborative documents where granular updates can be merged deterministically.
- Server-Mediated Rejection: The server detects
version mismatches using
ETagheaders or version counters and asks the client to pull the latest state and reconcile changes manually.
Fallback Strategies for Cross-Browser Compatibility
The Background Sync API is fully supported in Chromium-based browsers, but support varies across other engines like WebKit (Safari). Teams maintain resilience across all platforms by implementing progressive enhancement:
- Online Event Listeners: Listen for
window.addEventListener('online')to immediately trigger sync operations if the tab remains open. - Page Load Handlers: Check the IndexedDB outbox on every page initialization to process any unsynced actions from previous sessions.
- Periodic Background Sync: For supported browsers, register periodic sync tasks to fetch updates and push pending states at regular intervals.