· deepdives · 8 min read
Mastering Background Sync API: The Secret Sauce to Reliable Offline Capabilities
Learn how to use the Background Sync API to keep your web app working reliably during network disruptions. This tutorial includes step-by-step code, queueing patterns, Workbox integration, periodic sync, debugging tips and best practices.

Achieve reliable offline behavior - even when the network fails
Imagine users tapping “send” and moving on, confident your app will deliver once connectivity returns. No confusing error messages. No lost data. Just a smooth experience.
This guide shows you how to use the Background Sync API to make that happen. You’ll get hands‑on examples for one‑off syncs, an IndexedDB-backed queue to replay failed requests, a Workbox shortcut, and a look at Periodic Background Sync for scheduled background work.
What you’ll be able to do by the end:
- Queue outgoing network requests when offline
- Register and handle sync events in the service worker to replay those requests
- Use Workbox for an easier setup
- Understand Periodic Background Sync and when to use it
- Apply best practices for retries, idempotency, and user feedback
Quick primer: what Background Sync actually is
There are two related features:
- One‑off Background Sync (SyncManager): lets a service worker ask the browser to wake it when connectivity is restored and run a named sync task once.
- Periodic Background Sync: lets you register recurring background tasks that run at intervals (useful for periodic content refresh). This is more constrained and permissioned.
Background Sync is not a magic channel to run long tasks - it gives you a chance to do short, controlled work when the browser decides conditions (connectivity, battery, policy) are appropriate.
Browser support is partial; always feature‑detect and provide graceful fallbacks. See browser compatibility below and the references at the end.
How one‑off Background Sync works (high level)
- The page tries a network request. If it fails (offline), it stores the request details in an offline queue (commonly in IndexedDB).
- The page registers a sync with a tag via the Service Worker registration: registration.sync.register(‘mySyncTag’).
- When the browser decides, it wakes the service worker and fires a
syncevent with that tag. - The service worker opens the queue, replays requests, removes successful items, and may re-register sync if items remain.
This is robust: users can continue working offline; requests are sent reliably in the background when connectivity returns.
Minimal example - queue failed POSTs and replay them
We’ll build a simple flow:
- main.js: when fetch fails, save request to a queue and register a sync event.
- service-worker.js: handle
syncevents, replay the queue.
Notes:
- Keep requests idempotent if possible (or include unique IDs server-side).
- Limit payload size stored locally.
1) A tiny IndexedDB helper (idb-lite)
Use a small wrapper over IndexedDB for clarity. In production, consider the idb library for robust APIs.
// db.js - minimal promise-based IndexedDB helper
export function openDb(name = 'offline-requests', store = 'requests') {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, 1);
req.onupgradeneeded = () => {
req.result.createObjectStore(store, {
keyPath: 'id',
autoIncrement: true,
});
};
req.onsuccess = () => resolve({ db: req.result, storeName: store });
req.onerror = () => reject(req.error);
});
}
export async function addRequest(data) {
const { db, storeName } = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
tx.objectStore(storeName).add(data);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
export async function getAllRequests() {
const { db, storeName } = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readonly');
const req = tx.objectStore(storeName).getAll();
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
export async function deleteRequest(id) {
const { db, storeName } = await openDb();
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, 'readwrite');
tx.objectStore(storeName).delete(id);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}2) Register failed requests from the page (main.js)
// main.js
async function sendData(url, payload) {
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error('Server error');
return await res.json();
} catch (err) {
// Offline or failed. Queue the request and register sync.
await addRequest({ url, payload, method: 'POST', timestamp: Date.now() });
const reg = await navigator.serviceWorker.ready;
try {
await reg.sync.register('sync-offline-requests');
console.log('Sync registered');
} catch (e) {
// Sync unavailable. Consider fallback (retry later via app logic)
console.warn('Background sync failed to register', e);
}
}
}3) Service worker: listen for sync, replay queue (service-worker.js)
importScripts('/db.js'); // or include helpers inside worker
self.addEventListener('sync', function (event) {
if (event.tag === 'sync-offline-requests') {
event.waitUntil(processQueue());
}
});
async function processQueue() {
const items = await getAllRequests();
for (const item of items) {
try {
const res = await fetch(item.url, {
method: item.method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item.payload),
});
if (res.ok) {
await deleteRequest(item.id);
} else {
// Server responded with error - don't delete; maybe schedule backoff
console.warn('Server error when replaying request', res.status);
}
} catch (err) {
// Network still failing - exit early. The browser will try sync later.
console.warn(
'Network error when replaying queue. Leaving items in queue'
);
return;
}
}
}Notes on the above:
event.waitUntil()keeps the service worker alive until the promise resolves.- If the queue is large, process items in controlled batches to avoid long running tasks.
Using Workbox Background Sync (easier for request replay)
If you use Workbox, the background sync plugin manages queueing and replay for you. It’s ideal for caching strategies and failed POSTs.
Example (service-worker.js with Workbox):
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
import { BackgroundSyncPlugin } from 'workbox-background-sync';
const bgSyncPlugin = new BackgroundSyncPlugin('myQueueName', {
maxRetentionTime: 24 * 60, // Retry for max of 24 hours (minutes)
});
registerRoute(
/\/api\/.*\/.*/,
new NetworkOnly({ plugins: [bgSyncPlugin] }),
'POST'
);Workbox takes care of storing requests (it serializes requests, bodies, headers) and replaying them. Good default behavior and less boilerplate.
Reference: Workbox Background Sync docs: https://developers.google.com/web/tools/workbox/modules/workbox-background-sync
Periodic Background Sync: scheduled background work
Periodic Background Sync is for use cases like: refresh content, pull fresh data for offline reading, or sync analytics. Example use:
// Request permission and register (main thread)
async function registerPeriodic() {
const status = await navigator.permissions.query({
name: 'periodic-background-sync',
});
if (status.state === 'granted') {
const reg = await navigator.serviceWorker.ready;
try {
await reg.periodicSync.register('get-latest-news', {
minInterval: 24 * 60 * 60 * 1000,
}); // 1 day
} catch (err) {
console.warn('Periodic sync registration failed', err);
}
} else {
console.warn('Periodic Background Sync permission:', status.state);
}
}And in the service worker:
self.addEventListener('periodicsync', event => {
if (event.tag === 'get-latest-news') {
event.waitUntil(refreshArticles());
}
});Caveats:
- Periodic sync is permissioned and limited by browser heuristics.
- Use it for light background refreshes, not large uploads.
Documentation: https://web.dev/periodic-background-sync/
Best practices and gotchas
- Idempotency: make server endpoints idempotent or include unique client-generated IDs so replayed requests don’t duplicate work.
- Small payloads: store small objects in IndexedDB; large files should use Background Fetch or chunked uploads.
- Batch and backoff: replay in small batches. If an error is non‑transient (4xx), remove or flag the request instead of retrying forever.
- Security: don’t keep long‑lived sensitive credentials in local storage. Use tokens that can be revoked and rotate them.
- Provide UX: show an indicator for synced/queued items; let users retry manually if needed.
- Respect worker lifetime: the browser may stop the worker if it runs too long. Use event.waitUntil() and keep tasks short.
- Test offline behavior: simulate offline in DevTools and verify queue storage and replay. See debugging tips below.
- Fallback: if Background Sync isn’t available, expose a manual retry mechanism (e.g., “Retry” button, or periodic retry when the app opens).
Debugging and testing tips
- Chrome DevTools: Application > Service Workers to inspect service workers and background sync registrations. You can also trigger background sync manually in the “Service Workers” pane.
- Console logs: include clear logs in the service worker and page to trace queue and sync lifecycle.
- Simulate offline: DevTools -> Network -> Offline. Test adding items and then re-enabling network to confirm sync behavior.
- Inspect IndexedDB: DevTools > Application > IndexedDB to verify queued items.
- Use Workbox’s debugging features when applicable.
Browser support and fallbacks
- One‑off Background Sync: supported in Chromium-based browsers (Chrome, Edge). Support in Firefox is partial or behind flags historically.
- Periodic Background Sync: more limited and often behind flags/permissions.
Always feature‑detect:
if ('serviceWorker' in navigator && 'SyncManager' in window) {
// safe to use background sync
}If unavailable: implement fallback strategies such as manual retry buttons, optimistic UI with server reconciliation, or in‑app retry loops when the user returns online.
Reference charts: MDN and web.dev pages linked below.
Practical checklist before you ship
- Make write endpoints idempotent or attach unique request IDs
- Limit queued request sizes; cap queue length
- Show queue status in UI and let user clear if needed
- Add server-side deduplication for safety
- Use feature detection and provide a graceful fallback
- Test across devices and network conditions
Conclusion
Background Sync transforms intermittent connectivity from a show‑stopper into a user experience problem you can solve. With a small queue, clear idempotency rules, and careful service worker handling, your app can deliver reliable offline behavior and sync automatically when conditions permit.
Use Workbox to reduce boilerplate. Use Periodic Background Sync for scheduled background refreshes. And always design with constraints in mind: short tasks, small payloads, and good user feedback.
References
- MDN: Background Sync API - https://developer.mozilla.org/en-US/docs/Web/API/Background_Sync_API
- Google Web Dev: Using the Background Sync API - https://web.dev/background-sync/
- Google Web Dev: Periodic Background Sync - https://web.dev/periodic-background-sync/
- Workbox Background Sync - https://developers.google.com/web/tools/workbox/modules/workbox-background-sync



