Progressive Web Apps (PWAs) have revolutionized the way we experience web applications by offering offline capabilities, push notifications, and the ability to be installed on a user’s home screen. One of the standout features that can significantly enhance the user experience is Background Sync. This feature allows your PWA to defer tasks until the user has a stable internet connection, ensuring data integrity and improving reliability. In this article, we will explore how to implement Background Sync in PWAs, detailing the steps and best practices to make the most out of this powerful functionality.
Understanding Background Sync
What is Background Sync?
Background Sync is a feature in Progressive Web Apps that allows you to postpone certain tasks until the user has a reliable network connection. This is particularly useful for tasks like form submissions, data synchronization, or any operation that requires a stable internet connection. By deferring these tasks, you can enhance the user experience by ensuring that actions taken while offline are eventually completed once connectivity is restored.
The primary benefit of Background Sync is its ability to handle intermittent connectivity gracefully. For example, if a user submits a form while offline, the service worker can intercept this request and wait until the network is available to send the data. This ensures that no data is lost and that the user can continue to interact with the app without interruption.
How Does Background Sync Work?
Background Sync works through service workers, which are scripts that run in the background, separate from your web page. When a user performs an action that requires network access, the service worker can register a sync event. The service worker then stores the request and waits for the browser to signal that a network connection is available.
Once the network is available, the service worker processes the stored requests, ensuring that the actions are completed successfully. This process involves using the SyncManager interface, which provides methods to register sync events and listen for connectivity changes. By leveraging this interface, developers can create robust offline experiences that seamlessly synchronize data when the user is back online.
Setting Up a Service Worker for Background Sync
Registering the Service Worker
To implement Background Sync, you first need to set up a service worker. A service worker is a JavaScript file that runs in the background and intercepts network requests, caching resources, and handling offline functionality. Begin by registering your service worker in your main JavaScript file:
if ('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('Service Worker registered with scope:', registration.scope);
})
.catch(error => {
console.log('Service Worker registration failed:', error);
});
} else {
console.log('Service Worker or Background Sync not supported');
}
This code checks if the browser supports service workers and the SyncManager interface, then registers the service worker if supported. If either is not supported, it logs a message to the console.
Writing the Service Worker Script
Next, you need to create the service worker script (service-worker.js
). This script will handle the installation, activation, and fetch events, as well as manage the Background Sync functionality. Start by writing a basic service worker script:
self.addEventListener('install', event => {
console.log('Service Worker installing...');
// Add assets to cache here if needed
});
self.addEventListener('activate', event => {
console.log('Service Worker activating...');
// Clean up old caches if needed
});
self.addEventListener('fetch', event => {
// Intercept network requests and serve cached responses if available
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request);
})
);
});
This basic service worker script handles the installation and activation events and intercepts network requests to serve cached responses. You can expand this script to include caching strategies and additional functionality as needed.
Implementing Background Sync Functionality
Registering a Sync Event
To implement Background Sync, you need to register a sync event in your service worker. This involves listening for specific actions, such as form submissions or data updates, and registering a sync event when these actions occur. Modify your service worker script to include the following code:
self.addEventListener('fetch', event => {
if (event.request.method === 'POST' && event.request.url.endsWith('/submit')) {
event.respondWith(
fetch(event.request).catch(() => {
return saveRequest(event.request).then(() => {
return self.registration.sync.register('sync-requests');
});
})
);
} else {
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request);
})
);
}
});
function saveRequest(request) {
return request.clone().text().then(body => {
const requestRecord = {
url: request.url,
method: request.method,
headers: [...request.headers],
body: body
};
return idbKeyval.set('sync-requests', requestRecord);
});
}
This code snippet intercepts POST requests to a specific URL (/submit
), attempts to fetch the request, and if the network is unavailable, saves the request using IndexedDB (via a library like idb-keyval) and registers a sync event.
Handling Sync Events
Once you have registered a sync event, you need to handle it in your service worker. Add the following code to your service worker script to listen for sync events and process the stored requests:
self.addEventListener('sync', event => {
if (event.tag === 'sync-requests') {
event.waitUntil(
idbKeyval.get('sync-requests').then(requestRecord => {
return fetch(requestRecord.url, {
method: requestRecord.method,
headers: new Headers(requestRecord.headers),
body: requestRecord.body
}).then(response => {
if (response.ok) {
return idbKeyval.del('sync-requests');
}
});
})
);
}
});
This code listens for sync events with the tag sync-requests
, retrieves the stored request from IndexedDB, and attempts to resend it. If the request is successful, it deletes the request record from IndexedDB.

Enhancing Background Sync for Robustness
Handling Multiple Requests
In real-world applications, users might perform multiple actions while offline that need to be synced once the network is available. To handle this scenario, you need to store and manage multiple requests. Instead of saving a single request, you can use an array to keep track of all pending requests. Modify the saveRequest
function to add requests to an array in IndexedDB:
function saveRequest(request) {
return request.clone().text().then(body => {
const requestRecord = {
url: request.url,
method: request.method,
headers: [...request.headers],
body: body
};
return idbKeyval.get('sync-requests').then(requests => {
requests = requests || [];
requests.push(requestRecord);
return idbKeyval.set('sync-requests', requests);
});
});
}
Update the sync event handler to process all stored requests:
self.addEventListener('sync', event => {
if (event.tag === 'sync-requests') {
event.waitUntil(
idbKeyval.get('sync-requests').then(requests => {
return Promise.all(requests.map(requestRecord => {
return fetch(requestRecord.url, {
method: requestRecord.method,
headers: new Headers(requestRecord.headers),
body: requestRecord.body
}).then(response => {
if (response.ok) {
return requestRecord;
}
});
})).then(successfulRequests => {
const failedRequests = requests.filter(req => !successfulRequests.includes(req));
return idbKeyval.set('sync-requests', failedRequests);
});
})
);
}
});
This code processes all pending requests and only retains those that fail, ensuring that successful requests are removed from the array.
Retry Mechanisms and Error Handling
To make your Background Sync implementation more robust, it’s important to handle errors and retry failed requests. You can implement retry mechanisms to attempt sending requests multiple times before giving up. Enhance the saveRequest
function to include a retry count:
function saveRequest(request) {
return request.clone().text().then(body => {
const requestRecord = {
url: request.url,
method: request.method,
headers: [...request.headers],
body: body,
retries: 0
};
return idbKeyval.get('sync-requests').then(requests => {
requests = requests || [];
requests.push(requestRecord);
return idbKeyval.set('sync-requests', requests);
});
});
}
Update the sync event handler to increment the retry count and remove requests that have exceeded the retry limit:
self.addEventListener('sync', event => {
if (event.tag === 'sync-requests') {
event.waitUntil(
idbKeyval.get('sync-requests').then(requests => {
return Promise.all(requests.map(requestRecord => {
return fetch(requestRecord.url, {
method: requestRecord.method,
headers: new Headers(requestRecord.headers),
body: requestRecord.body
}).then(response => {
if (response.ok) {
return requestRecord;
} else {
requestRecord.retries += 1;
return requestRecord;
}
}).catch(() => {
requestRecord.retries += 1;
return requestRecord;
});
})).then(processedRequests => {
const failedRequests = processedRequests.filter(req => req.retries < 3);
return idbKeyval.set('sync-requests', failedRequests);
});
})
);
}
});
This code increments the retry count for each failed request and removes requests that have exceeded a maximum retry limit (e.g., 3 retries). This ensures that the service worker does not repeatedly attempt to send requests that consistently fail.
Testing Background Sync
Simulating Offline Scenarios
To ensure your Background Sync implementation works correctly, you need to test it in various scenarios, including when the app is offline. You can simulate offline scenarios using Chrome DevTools. Open DevTools, go to the “Network” tab, and select “Offline” from the throttling options. This allows you to test how your PWA behaves when there is no network connection.
Perform actions such as form submissions or data updates while offline and verify that they are queued and synchronized when the network is restored. Check the service worker logs and IndexedDB to ensure that requests are being saved and processed as expected.
Monitoring Sync Events
Monitoring sync events and the behavior of your service worker is crucial for debugging and optimizing your Background Sync implementation. Use the “Application” tab in Chrome DevTools to inspect service worker registrations, sync events, and IndexedDB storage. This helps you understand how your service worker handles sync events and manage pending requests.
Additionally, use console logging to track the flow of your sync events and identify any issues. For example, log messages when requests are saved, when sync events are triggered, and when requests are successfully processed or retried. This detailed logging can help you pinpoint problems and ensure that your Background Sync implementation is robust and reliable.

Best Practices for Background Sync
Optimize Data Storage
Efficiently managing the storage of pending requests is crucial for the performance and reliability of your PWA. Use IndexedDB to store requests and manage them efficiently. Ensure that your data storage is optimized to handle large volumes of requests without affecting performance.
Consider using libraries like idb-keyval for a simple and effective way to manage IndexedDB operations. Regularly clean up outdated or unnecessary data to keep your storage efficient and prevent it from becoming cluttered.
Provide User Feedback
Providing users with feedback about the status of their actions is important for a good user experience. Inform users when their actions are queued for syncing, and notify them once the actions are successfully completed. This can be done through UI elements like toast notifications or status indicators.
Clear and timely feedback reassures users that their actions are being processed and helps them understand the behavior of your PWA. It also enhances trust and engagement by ensuring users are aware of what is happening in the background.
Advanced Use Cases for Background Sync
Syncing Complex Data
In addition to handling simple form submissions, Background Sync can be used to synchronize more complex data structures, such as JSON objects or multipart form data. This is particularly useful for applications that handle large amounts of data, such as task management tools, note-taking apps, or any application where users can generate substantial content while offline.
To handle complex data, ensure that your service worker can serialize and deserialize the data correctly when saving it to IndexedDB. For instance, if you are dealing with JSON data, you can store the stringified JSON in IndexedDB and parse it when retrieving it for synchronization.
function saveRequest(request) {
return request.clone().json().then(body => {
const requestRecord = {
url: request.url,
method: request.method,
headers: [...request.headers],
body: JSON.stringify(body),
retries: 0
};
return idbKeyval.get('sync-requests').then(requests => {
requests = requests || [];
requests.push(requestRecord);
return idbKeyval.set('sync-requests', requests);
});
});
}
When retrieving the data for synchronization, ensure you parse the JSON string back into an object:
self.addEventListener('sync', event => {
if (event.tag === 'sync-requests') {
event.waitUntil(
idbKeyval.get('sync-requests').then(requests => {
return Promise.all(requests.map(requestRecord => {
const body = JSON.parse(requestRecord.body);
return fetch(requestRecord.url, {
method: requestRecord.method,
headers: new Headers(requestRecord.headers),
body: JSON.stringify(body)
}).then(response => {
if (response.ok) {
return requestRecord;
} else {
requestRecord.retries += 1;
return requestRecord;
}
}).catch(() => {
requestRecord.retries += 1;
return requestRecord;
});
})).then(processedRequests => {
const failedRequests = processedRequests.filter(req => req.retries < 3);
return idbKeyval.set('sync-requests', failedRequests);
});
})
);
}
});
This ensures that your service worker can handle and synchronize complex data structures efficiently.
Handling Large File Uploads
For applications that involve large file uploads, Background Sync can ensure that these uploads are completed successfully even if the user experiences connectivity issues. Large file uploads can be split into smaller chunks and uploaded in parts. This way, if the network goes down, only the remaining chunks need to be uploaded when connectivity is restored.
To implement this, you can use the Blob
and File
interfaces in JavaScript to split large files into smaller chunks and store them in IndexedDB. Your service worker can then handle the synchronization of these chunks:
function saveFileChunks(file) {
const chunkSize = 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
let chunkPromises = [];
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
chunkPromises.push(new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const chunkRecord = {
index: i,
totalChunks: totalChunks,
fileName: file.name,
chunkData: reader.result
};
resolve(chunkRecord);
};
reader.onerror = reject;
reader.readAsDataURL(chunk);
}));
}
return Promise.all(chunkPromises).then(chunks => {
return idbKeyval.get('file-chunks').then(storedChunks => {
storedChunks = storedChunks || [];
storedChunks.push(...chunks);
return idbKeyval.set('file-chunks', storedChunks);
});
});
}
self.addEventListener('sync', event => {
if (event.tag === 'sync-file-chunks') {
event.waitUntil(
idbKeyval.get('file-chunks').then(chunks => {
return Promise.all(chunks.map(chunkRecord => {
return fetch(`/upload/${chunkRecord.fileName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: chunkRecord.chunkData
}).then(response => {
if (response.ok) {
return chunkRecord;
} else {
chunkRecord.retries = (chunkRecord.retries || 0) + 1;
return chunkRecord;
}
}).catch(() => {
chunkRecord.retries = (chunkRecord.retries || 0) + 1;
return chunkRecord;
});
})).then(processedChunks => {
const failedChunks = processedChunks.filter(chunk => (chunk.retries || 0) < 3);
return idbKeyval.set('file-chunks', failedChunks);
});
})
);
}
});
This code handles the splitting of large files into smaller chunks, stores these chunks in IndexedDB, and ensures they are uploaded in parts when the network is available.
Monitoring and Analytics
Tracking Background Sync Events
To gain insights into how Background Sync is performing in your PWA, it’s crucial to track sync events and their outcomes. By collecting data on sync events, you can identify issues, monitor performance, and improve the reliability of your Background Sync implementation.
You can use analytics tools such as Google Analytics to track Background Sync events. For example, you can log custom events in your service worker to monitor sync success and failures:
function logSyncEvent(event, status) {
fetch('/log-sync-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event, status })
});
}
self.addEventListener('sync', event => {
if (event.tag === 'sync-requests') {
event.waitUntil(
idbKeyval.get('sync-requests').then(requests => {
return Promise.all(requests.map(requestRecord => {
return fetch(requestRecord.url, {
method: requestRecord.method,
headers: new Headers(requestRecord.headers),
body: requestRecord.body
}).then(response => {
if (response.ok) {
logSyncEvent('sync-requests', 'success');
return requestRecord;
} else {
requestRecord.retries += 1;
logSyncEvent('sync-requests', 'failure');
return requestRecord;
}
}).catch(() => {
requestRecord.retries += 1;
logSyncEvent('sync-requests', 'failure');
return requestRecord;
});
})).then(processedRequests => {
const failedRequests = processedRequests.filter(req => req.retries < 3);
return idbKeyval.set('sync-requests', failedRequests);
});
})
);
}
});
This example logs sync event statuses to your server, which you can then analyze using your chosen analytics platform.
Analyzing Sync Performance
Once you have collected sync event data, analyze it to identify patterns and areas for improvement. Look for trends in sync failures, such as specific types of requests that frequently fail or common retry counts. Use this information to refine your sync logic, improve error handling, and enhance overall performance.
Additionally, monitor the impact of Background Sync on user engagement and satisfaction. Track metrics such as the number of successfully completed sync events, average sync times, and user retention rates. This analysis helps you understand the effectiveness of your Background Sync implementation and make data-driven decisions to optimize your PWA.
Conclusion
Implementing Background Sync in your Progressive Web App can significantly enhance the user experience by ensuring that critical tasks are completed even when the user is offline. By setting up a service worker, registering sync events, handling multiple requests, implementing retry mechanisms, and following best practices, you can create a robust and reliable Background Sync solution.
We hope this comprehensive guide has provided valuable insights and actionable steps to help you implement Background Sync in your PWA. If you have any questions or need further assistance, feel free to reach out. Thank you for reading, and best of luck with your Progressive Web App development journey!
Read Next: