10LOC
Chrome APIsintermediate

Background Sync for offline-friendly actions

Published July 23, 2027

type SyncRegistration = ServiceWorkerRegistration & { sync: { register: (tag: string) => Promise<void> } };

export const scheduleOfflineAction = async (tag: string, replayNow: () => Promise<void>): Promise<void> => {
  const registration = await navigator.serviceWorker.ready;
  const canBackgroundSync = "sync" in registration;
  if (canBackgroundSync) {
    await (registration as SyncRegistration).sync.register(tag);
    return;
  }
  await replayNow();
};

What

registration.sync.register(tag) asks the browser to fire a sync event in the service worker as soon as the device has network connectivity again — even if the tab that queued the action has since been closed.

Why it matters

The usual offline pattern is: catch the failed fetch, stash the action in IndexedDB, and retry on the browser's online event. That works only while the tab stays open — close it, and the online listener never fires, so the queued action sits there until the user happens to reopen the app. Background Sync moves the retry trigger into the service worker, which the browser can wake up independently of any open tab, so a form submitted while offline on a phone still goes out later even if the user swiped the tab away. The catch is real: only Chromium browsers implement it, so scheduleOfflineAction treats it as progressive enhancement, not the only path — replayNow() is the fallback for every browser that doesn't have it.

How it works

  • SyncRegistration extends ServiceWorkerRegistration with the sync property TypeScript's DOM types don't include (see Gotchas); the cast is applied only once support is confirmed via "sync" in registration.
  • scheduleOfflineAction checks for sync support before doing anything else — Firefox and Safari fail this check and fall straight through to calling replayNow() immediately, which should attempt the action right away (and let normal error handling/retry logic take over from there).
  • When sync is supported, registration.sync.register(tag) just files the tag with the browser — it does not run any code itself. The actual retry logic lives in the service worker's own sync event listener: self.addEventListener("sync", (event) => { if (event.tag === tag) event.waitUntil(replayQueuedAction()); }). That handler runs in a different execution context (the service worker's global scope, not the page), so it can't be part of this same module — it has to live in the service worker script the browser loads separately.
  • event.waitUntil() in that handler is what keeps the service worker alive until the replay finishes; letting the promise it's given reject tells the browser the sync failed, which triggers another retry attempt later on the browser's own schedule.

Gotchas

  • Browser support is narrow: Chrome since version 49, Edge since 79 — and Firefox and Safari have no SyncManager support at all, with no public signal either intends to add it. Treat the fallback path as required, not optional polish.
  • Registering the same tag again before the first one has fired coalesces into a single pending sync — you don't get a queue of N pending syncs from N calls with the same tag.
  • Retry timing after a failed sync is entirely up to the browser's own backoff — there's no API to control or observe the schedule, only to react when the event eventually fires again.
  • registration.sync only exists once a service worker is active — navigator.serviceWorker.ready is what guarantees that, rather than checking navigator.serviceWorker.controller or racing the registration.

Browser support: Chrome/Edge only, as noted above — this is not Baseline. Always ship the replayNow() (or equivalent) fallback for Firefox and Safari.

Related

  • Periodic Background Sync — the recurring counterpart for scheduled refresh work, versus this API's one-shot "retry when back online."
  • IndexedDB — where the actual queued action data typically lives; this API only controls when the service worker wakes up to process that queue, not how the queue is stored.