Skip to content

Background Sync

Most offline strategies focus on reading cached content. But what about writes — a form submission, a chat message, a purchase? If the user submits data while offline, a naive implementation either shows an error or silently drops the request. The Background Sync API solves this by letting you queue the operation and have the service worker replay it automatically once connectivity returns, even if the user has already closed the tab.

The flow has three actors: the page, the service worker, and the browser’s sync infrastructure.

  1. The page detects a failed (or potentially offline) write.
  2. The page calls registration.sync.register('tag-name') to schedule a sync event.
  3. The browser fires a sync event on the service worker when it has a stable connection. The SW retries the operation and calls event.waitUntil() so the browser keeps it alive until the work is done.
  4. If the sync attempt fails, the browser retries with exponential back-off.

Background Sync is currently supported only in Chromium-based browsers (Chrome, Edge, Samsung Internet, and most Android browsers). Firefox and Safari do not implement it. This makes graceful fallback essential — if the API is not available, you should attempt the network request immediately.

async function registerSync(registration) {
if ('SyncManager' in window && registration.sync) {
await registration.sync.register('send-form');
console.log('Sync registered — will retry when online');
} else {
// Browser does not support Background Sync; attempt immediately
console.log('Background Sync not supported — sending now');
await sendQueuedData();
}
}

Check for both 'SyncManager' in window and registration.sync — older Chromium versions exposed SyncManager but not registration.sync.

When the user submits a form, save the data to IndexedDB first, then register the sync tag. The write to IndexedDB is the durable record; the sync tag is just a signal to the service worker.

async function handleSubmit(formData) {
// 1. Save to IndexedDB so the SW can read it later
await saveToIdb('outbox', { id: Date.now(), ...formData });
// 2. Get the SW registration
const registration = await navigator.serviceWorker.ready;
if (registration.sync) {
// 3. Register the background sync tag
await registration.sync.register('send-form');
} else {
// Fallback: try the request right now
await sendQueuedData();
}
}

Service worker side: replaying queued data

Section titled “Service worker side: replaying queued data”

The service worker listens for the sync event. The event.tag property matches the string passed to registration.sync.register().

self.addEventListener('sync', (event) => {
if (event.tag === 'send-form') {
event.waitUntil(replayOutbox());
}
});
async function replayOutbox() {
const db = await openDatabase();
const tx = db.transaction('outbox', 'readwrite');
const store = tx.objectStore('outbox');
const allRequests = await getAllRecords(store);
for (const item of allRequests) {
const response = await fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
});
if (response.ok) {
// Remove from outbox after successful send
await deleteRecord(store, item.id);
}
}
}

If replayOutbox() throws or returns a rejected Promise, the browser treats the sync as failed and schedules a retry. Only resolve successfully once you are sure the data has been delivered.

sequenceDiagram
  participant U as User
  participant P as Page
  participant IDB as IndexedDB
  participant SW as Service Worker
  participant API as Server API

  U->>P: Submits form (offline)
  P->>IDB: saveToIdb('outbox', data)
  P->>SW: registration.sync.register('send-form')
  Note over U,SW: Connection restored
  SW->>SW: sync event fires (tag: send-form)
  SW->>IDB: read all outbox records
  IDB-->>SW: queued items
  SW->>API: POST /api/submit
  API-->>SW: 200 OK
  SW->>IDB: delete sent record
Background Sync: queue while offline, replay when connected

The playground below demonstrates the full flow. The page lets you submit a message. The service worker queues it to an in-memory outbox and registers a sync tag. When the browser fires the sync event the SW logs the replay. Toggle the browser’s Network panel to “Offline” before submitting to see the queue in action.

Runs a real service worker + manifest in your browser.
Which method does the page call to schedule a background sync?
In which browsers is the Background Sync API currently supported?
What should the service worker do if the sync attempt fails (the network request throws)?
Why is data saved to IndexedDB before registering the sync tag?