skip to content

What does the Background Sync API's `sync` event give you that retrying a failed request inside the page does not, and how do you register one?

level: middleimportance: should knowfreq 34%

answer

  1. retries that outlive the tab
  2. a tag, not a payload
  3. coalesces on the same tag
  4. reject means try again later
  5. Chromium-only, so it is a bonus

basics

~20 s

Background Sync hands a retry to the browser: you call registration.sync.register('tag') and the browser fires a sync event in the service worker once it has connectivity, even after the page is closed. Page-level retries die with the tab.

solid answer

~40 s

A retry loop in the page is bound to the page. Close the tab, navigate away, or lose the process and the queued write is gone. Background Sync moves the retry to the browser: `const reg = await navigator.serviceWorker.ready; await reg.sync.register('outbox');` registers a one-off tag, and the browser fires a `sync` event in the service worker when it believes there is connectivity — possibly minutes later, with no page open. Inside the handler you call `event.waitUntil(flushOutbox())`; if that promise rejects the browser schedules another attempt with backoff, and `event.lastChance` is true on the final one. Registering the same tag repeatedly coalesces into one pending sync rather than queueing duplicates. It is a Chromium-only API as of 2025, so it must be treated as an enhancement over a working foreground path.

code

javascript · 23 lines
javascript
// page
async function queueWrite(record) {
  await saveToOutbox(record); // durable store, e.g. IndexedDB
  const registration = await navigator.serviceWorker.ready;
  if ('sync' in registration) {
    await registration.sync.register('outbox-flush');
  } else {
    void flushOutbox();
  }
}

// service worker
self.addEventListener('sync', (event) => {
  if (event.tag !== 'outbox-flush') return;
  event.waitUntil(
    flushOutbox().catch((error) => {
      if (event.lastChance) {
        console.error('outbox flush permanently failed', error);
      }
      throw error; // rejecting asks the browser to retry later
    })
  );
});

go deeper

for a junior

Know that a retry written in the page dies when the tab closes, and that Background Sync asks the browser to run the retry in the service worker instead.

for a middle

Explain register(tag), tag coalescing, waitUntil deciding retry versus done, and lastChance marking the final attempt. Say that the payload lives in storage, not in the registration.

for a senior

Bring up idempotency keys unprompted, since the browser may run the handler repeatedly, and describe how you surface a permanent failure to the user on lastChance instead of dropping the write.

for a principal

Own the reliability contract this implies: what the UI is allowed to claim about a queued write, how long a queued item may live before it is stale, and what happens to it on browsers without the API.

## The problem A user taps *Save* in a tunnel. The request fails. You can retry in the page — a timer, a listener on the `online` event — but every one of those strategies has the same defect: it lives in the document. The user closes the tab, the OS reclaims the process, or they simply switch apps and the tab is discarded. The write is lost, and worse, it was shown as accepted. Background Sync exists to move that responsibility to the browser, which outlives the page. ## Registering ```js async function queueWrite(record) { await saveToOutbox(record); // durable local store first const registration = await navigator.serviceWorker.ready; if ('sync' in registration) { await registration.sync.register('outbox-flush'); } else { void flushOutboxNow(); // fallback path } } ``` `registration.sync` is a `SyncManager`. `register(tag)` takes a string tag; registering the same tag again while one is pending coalesces — you get one pending sync, not two. That is a feature: the tag names *work that needs doing*, not an individual item, so the handler should drain a queue rather than expect a payload. There is no way to attach data to a registration, which is why the durable store comes first and the tag merely says "there is something to flush". ## Handling ```js self.addEventListener('sync', (event) => { if (event.tag !== 'outbox-flush') return; event.waitUntil( flushOutbox().catch((err) => { if (event.lastChance) reportPermanentFailure(err); throw err; // reject → browser retries }) ); }); ``` Three properties of the event carry the semantics. `event.tag` identifies which registration woke you — one listener typically serves several tags. `event.waitUntil(promise)` both keeps the service worker alive while the work runs and reports the outcome: **resolve means done, and the registration is cleared; reject means not done, and the browser will try again** with backoff over a limited window before giving up. `event.lastChance` is `true` on that final attempt, which is your cue to surface a durable failure to the user rather than silently dropping the work. The browser may fire the event almost immediately if the device is already online, so the handler must be correct in the trivial case too, not only after a real outage. ## What this demands of the request Because the browser decides how many times to run your handler, every operation it performs must be safe to repeat. A `POST /orders` that creates a row per call will create duplicates the first time a response is lost after the server processed it. The standard answer is an idempotency key generated when the record enters the outbox and sent with each attempt, so the server can recognise a replay. This is the part candidates most often miss: Background Sync makes retries *more* likely, so it raises the bar on idempotency rather than lowering it. ## Periodic Background Sync is a different thing `registration.periodicSync.register('news', { minInterval: 24 * 60 * 60 * 1000 })` fires a `periodicsync` event on a browser-chosen cadence, for refreshing content rather than flushing writes. It is gated far more tightly — it generally requires an installed app, is subject to a `periodic-background-sync` permission and to the browser's engagement heuristics, and the `minInterval` is a floor the browser is free to ignore upward. Do not confuse the two in an answer: one-off `sync` is for work the user already asked for; `periodicsync` is for speculative freshness. ## Support As of 2025 `SyncManager` is implemented in Chromium browsers only; Safari and Firefox do not ship it. That does not make it useless — it means the design is a ladder. The outbox in durable storage and a foreground flush are the base everyone gets; Background Sync is the extra reliability Chromium users receive on top. Feature-detect with `'sync' in registration` and never let the base path depend on it.

  • Why does the sync registration take only a string tag with no payload?
    Because the event may fire long after the page is gone, so the data must already be in durable storage that the service worker can read — typically an IndexedDB outbox. The tag names the job. It also explains coalescing: repeat registrations of one tag mean "there is still work", not "do it twice".
  • What must be true of the requests a sync handler sends, and why?
    They must be idempotent. The browser can invoke the handler several times, and a response lost after the server committed is indistinguishable from a failure. Attach an idempotency key when the item enters the outbox and send it on every attempt so the server can collapse replays into one effect.
  • How does `periodicsync` differ from the one-off `sync` event?
    `sync` fires once for work the user already initiated and retries until it succeeds or the browser gives up. `periodicsync` fires repeatedly on a browser-chosen cadence to refresh content speculatively, requires an installed app and a separate permission, and treats your `minInterval` as a floor it may exceed at will.
  • The device is online when you call `sync.register()`. When does the event fire?
    Potentially immediately — Background Sync is not a delay mechanism. The handler therefore runs in the ordinary case as well as after an outage, so it must tolerate being invoked while the page that queued the work is still open, and must not assume the user has gone away.

saying these in an interview costs you the question

  • Thinking sync.register() accepts the request body as data
  • Expecting each register() call to queue a separate sync
  • Assuming the event only fires after an offline period
  • Sending non-idempotent POSTs from a sync handler
  • Believing Background Sync works in every modern browser

context