Progressive Web Apps (PWA)
Implementing the Background Sync API for Data Integrity
Master the Background Sync API to defer network requests until a stable connection is available, preventing data loss during intermittent connectivity.
In this article
Solving the Intermittent Connectivity Problem
In the modern mobile landscape users frequently encounter areas of poor or non-existent connectivity known as Lie-fi where the device indicates a signal but cannot transmit data. Traditional web applications handle this failure by showing an error message or a loading spinner that eventually times out and forces the user to try again later. This approach is frustrating because it places the burden of reliability on the user rather than the software architecture.
The Background Sync API offers a way to decouple the user action from the network request by allowing the browser to defer tasks until a stable connection is established. Instead of failing immediately when the network is unavailable the application registers a sync request that the browser manages in the background even if the user navigates away from the page. This shift in perspective transforms the web from a fragile request-response environment into a resilient offline-first platform.
To build a mental model for this API think of it as a persistent outbox for your application. When a user submits a form or sends a message while offline the application places that data into a local queue and tells the browser to process it as soon as possible. The browser then takes responsibility for monitoring the network state and triggering the execution of the task at the optimal time.
Background Sync is not just about offline support; it is about building a guarantee of delivery that elevates web applications to the reliability standards of native software.
- Deferred execution of critical network tasks
- Guaranteed delivery even if the browser tab is closed
- Automatic retry logic managed by the browser engine
- Optimized battery and data usage through intelligent scheduling
The Lifecycle of a Sync Event
The lifecycle begins when the main thread requests a sync registration via the service worker registration object. This registration is associated with a unique tag which acts as an identifier for the specific type of background task you wish to perform. If the browser is currently online it will attempt to fire the sync event almost immediately after registration.
If the device is offline the browser stores the registration and monitors the connection status until the device returns to a functional state. Once connectivity is restored the service worker is woken up to handle the sync event. This ensures that the application logic runs in the background without requiring the user to keep the tab open or manually refresh the page.
1async function registerNotificationSync() {
2 const registration = await navigator.serviceWorker.ready;
3 try {
4 // Request a sync event with a specific tag name
5 await registration.sync.register('sync-pending-comments');
6 console.log('Background sync registered successfully');
7 } catch (err) {
8 // Background Sync might be unsupported or permission denied
9 console.error('Background sync registration failed:', err);
10 }
11}Architecting Persistence with IndexedDB
A common pitfall when implementing background sync is failing to persist the data that needs to be sent. The sync event itself does not carry a payload; it is merely a trigger that tells the service worker it is time to perform an action. Therefore you must store the data locally using a persistent storage mechanism like IndexedDB before registering the sync request.
IndexedDB is the ideal choice for this task because it offers significant storage capacity and is accessible from both the main thread and the service worker thread. When the user performs an action the application writes the details of that action to a store in IndexedDB. Only after the data is safely written should the application call the sync registration method.
This architectural pattern ensures that no data is lost even if the browser crashes or the device restarts between the time the user clicks a button and the time the network becomes available. The service worker then acts as a consumer of this local database reading the pending entries and attempting to synchronize them with the server. Once a synchronization task succeeds the service worker removes the corresponding entry from IndexedDB to prevent duplicate processing.
1async function queueCommentForSync(commentData) {
2 // Open the local database
3 const db = await openDatabase('AppStorage', 1);
4 const tx = db.transaction('outbox', 'readwrite');
5 const store = tx.objectStore('outbox');
6
7 // Store the actual data that needs to be sent later
8 await store.add({
9 ...commentData,
10 timestamp: Date.now(),
11 status: 'pending'
12 });
13
14 // Tell the service worker to check the outbox
15 const registration = await navigator.serviceWorker.ready;
16 return registration.sync.register('process-outbox');
17}Structuring the Outbox Pattern
The outbox pattern requires a clear schema to manage the state of each pending request effectively. Each entry should include a unique identifier a timestamp and the necessary payload for the API request such as the endpoint URL and the HTTP method. It is also helpful to include a retry count to identify requests that fail repeatedly due to server-side issues rather than network connectivity.
By treating the outbox as a formal queue you can ensure that messages are processed in the correct order. This is particularly important for applications where the sequence of operations matters such as a chat app or a collaborative document editor. Sorting by timestamp within the service worker ensures that older actions are reconciled before newer ones.
Implementing the Service Worker Listener
The heart of the background sync process lies within the service worker file where you listen for the sync event. When this event fires the service worker is granted a window of execution time to perform network requests. You must use the event dot waitUntil method to keep the service worker alive until the asynchronous synchronization tasks are completed.
The event dot waitUntil method accepts a promise as its argument and tells the browser not to terminate the service worker until that promise resolves or rejects. If the promise rejects the browser will automatically schedule a retry based on its internal backoff algorithm. This provides a robust safety net for handling transient errors without writing complex manual retry logic.
It is essential to handle different sync tags within the same listener to keep your code organized. You can use a switch statement to branch logic based on the tag property of the event. This allows a single service worker to manage multiple types of background tasks like uploading images and sending text updates using different synchronization strategies.
1self.addEventListener('sync', (event) => {
2 if (event.tag === 'process-outbox') {
3 // Ensure the worker stays alive until processing is done
4 event.waitUntil(processOutboxQueue());
5 }
6});
7
8async function processOutboxQueue() {
9 const db = await openDatabase('AppStorage', 1);
10 const pendingItems = await getAllFromStore(db, 'outbox');
11
12 for (const item of pendingItems) {
13 try {
14 const response = await fetch('/api/comments', {
15 method: 'POST',
16 body: JSON.stringify(item),
17 headers: { 'Content-Type': 'application/json' }
18 });
19
20 if (response.ok) {
21 // Remove successful items from the queue
22 await removeFromStore(db, 'outbox', item.id);
23 }
24 } catch (error) {
25 // Re-throw if it is a network error to trigger a browser retry
26 throw error;
27 }
28 }
29}Handling Partial Failures
In scenarios where the outbox contains multiple items a single failure should not necessarily block the entire queue. You must decide whether to stop processing and wait for the next sync event or to continue with the remaining items. Generally it is better to catch errors for individual items and only reject the main promise if the failure is systemic.
If an error is related to the data itself such as a validation error from the server retrying will never succeed. In these cases the application should move the failing item to a separate error log or mark it for user intervention rather than leaving it in the sync queue. This prevents the outbox from becoming clogged with invalid requests that waste resources.
Trade-offs and Operational Constraints
While the Background Sync API is powerful it is not a silver bullet for every synchronization need. Browser vendors impose certain limitations to protect the user's battery life and data plan. For example the browser may delay a sync event if the device is in low power mode or if the user has restricted background data usage.
Another critical consideration is the duration of the execution window. Service workers are intended to be short-lived and the browser will eventually kill a sync task if it takes too long to complete. For massive data transfers such as uploading high-resolution video the Background Fetch API is a more appropriate choice as it is designed for long-running transfers with progress indicators.
- One-off sync events are best for small payloads like JSON or form data
- The browser decides the retry frequency which is opaque to the developer
- Background Sync is currently primarily supported in Chromium-based browsers
- Permissions for background sync are often bundled with notifications or granted by default
Always design your backend APIs to be idempotent when working with background sync because network flakiness might cause the same request to be sent more than once.
Privacy and Security Considerations
Synchronizing data in the background can have privacy implications if sensitive information is transmitted without the user's active presence. Ensure that all data transmitted in the background is sent over secure HTTPS connections. You should also consider whether the user should be notified when a background task completes especially if it involves financial transactions.
Furthermore background sync can potentially reveal the user's online patterns or location through network activity. Developers should be transparent about how and when background tasks occur. Always respect user preferences and provide options to disable background synchronization if the application is used in data-sensitive environments.
