Periodic Background Sync vs One-Off Sync

The Background Sync family gives a service worker a way to do work after the page has closed, but it ships in two distinct flavours that solve different problems and are frequently confused. One-off Background Sync fires once when connectivity returns — ideal for flushing a queued action a user took offline. Periodic Background Sync fires on a recurring schedule to refresh content in the background — ideal for keeping cached data fresh. This guide, part of Background Sync API Implementation, contrasts the two, shows runnable registration and handler code for each, and lays out the browser-support reality that forces a fallback in production.

The headline constraint is the same for both: they are Chromium-only. Firefox and Safari implement neither, so any sync-dependent flow needs the online-event fallback described at the end of this guide.

One-off sync fires once on reconnect; periodic sync fires on a schedule A decision diagram contrasting the one-off sync trigger on reconnection with the periodic sync recurring trigger and their differing permission requirements. Service worker registration sync.register(tag) fires once when back online 'sync' event no permission needed periodicSync.register (tag, {minInterval}) recurring 'periodicsync' event installed PWA + permission

How the two triggers differ

One-off sync is event-driven: you register a tag while online or offline, and the browser fires a single sync event the next time it judges connectivity to be available — even if the page has been closed. It is designed for “deliver this thing once,” such as submitting a queued comment. Periodic sync is schedule-driven: you register a tag with a minInterval, and the browser fires periodicsync no more often than that interval, throttled by its own heuristics around battery, network, and how often the user engages with the installed app. It is designed for “keep this fresh,” such as pre-fetching today’s articles before the user opens the app.

Comparison table

Dimension One-off Background Sync Periodic Background Sync
Registration registration.sync.register(tag) registration.periodicSync.register(tag, {minInterval})
SW event sync periodicsync
Trigger Once, when connectivity returns Repeatedly, no sooner than minInterval
Permission None periodic-background-sync must be granted
Install requirement Works in a normal tab Requires an installed PWA
Typical use Flush a queued user action Refresh cached content in the background
Delivery guarantee Best-effort, retried by the browser Best-effort, browser-throttled, may never fire
Browser support Chromium only Chromium only

Registering one-off sync

You request a one-off sync from the page, then handle the sync event inside the service worker. The handler returns a promise via event.waitUntil; if it rejects, the browser retries the sync later.

// page.ts — request a one-off sync after queuing work offline.
async function requestOneOffSync(tag: string): Promise<void> {
  const reg = await navigator.serviceWorker.ready;
  if ('sync' in reg) {
    try {
      await reg.sync.register(tag);
    } catch (err) {
      // Registration can throw if permission is denied or the API is blocked.
      console.warn('One-off sync registration failed; will fall back', err);
      await flushQueueNow(); // immediate fallback path (see below)
    }
  } else {
    await flushQueueNow();
  }
}

async function flushQueueNow(): Promise<void> {
  // Direct attempt used when Background Sync is unavailable.
  // Real implementation drains the IndexedDB outbox over fetch().
}
// service-worker.ts — handle the one-off sync event.
declare const self: ServiceWorkerGlobalScope;

self.addEventListener('sync', (event: SyncEvent) => {
  if (event.tag === 'flush-outbox') {
    // waitUntil keeps the SW alive until the work settles; a rejection
    // tells the browser to retry the sync with its own backoff.
    event.waitUntil(drainOutbox());
  }
});

async function drainOutbox(): Promise<void> {
  // Read queued requests from IndexedDB and POST them; throw to force a retry.
}

Registering periodic sync with a permission query

Periodic sync requires the periodic-background-sync permission and an installed PWA. Query the permission first via navigator.permissions.query, register only if granted, and handle periodicsync in the service worker.

// page.ts — register periodic sync if permitted and supported.
async function registerPeriodicSync(
  tag: string,
  minIntervalMs: number,
): Promise<'registered' | 'denied' | 'unsupported'> {
  const reg = await navigator.serviceWorker.ready;
  if (!('periodicSync' in reg)) return 'unsupported';

  // Permission name is 'periodic-background-sync'.
  const status = await navigator.permissions.query({
    name: 'periodic-background-sync' as PermissionName,
  });
  if (status.state !== 'granted') return 'denied';

  try {
    await (reg as ServiceWorkerRegistration & {
      periodicSync: { register(tag: string, opts: { minInterval: number }): Promise<void> };
    }).periodicSync.register(tag, { minInterval: minIntervalMs });
    return 'registered';
  } catch {
    return 'unsupported';
  }
}

// Example: refresh at most once every 12 hours.
void registerPeriodicSync('refresh-articles', 12 * 60 * 60 * 1000);
// service-worker.ts — handle the periodicsync event.
declare const self: ServiceWorkerGlobalScope;

interface PeriodicSyncEvent extends ExtendableEvent {
  readonly tag: string;
}

self.addEventListener('periodicsync', (event: Event) => {
  const e = event as PeriodicSyncEvent;
  if (e.tag === 'refresh-articles') {
    e.waitUntil(refreshArticleCache());
  }
});

async function refreshArticleCache(): Promise<void> {
  const res = await fetch('/api/articles');
  if (!res.ok) return; // do not poison the cache with an error response
  const cache = await caches.open('articles-v1');
  await cache.put('/api/articles', res.clone());
}

Verification via DevTools

You do not have to wait for the browser to fire either event — Chromium’s DevTools can trigger them on demand:

  1. Open DevTools → Application → Service Workers. Confirm the worker is activated and running.
  2. For one-off sync, open the Background Services → Background Sync panel, click the record button, then use the Periodic Background Sync or Background Sync section to fire your registered tag and watch the event log.
  3. For periodic sync, the Application → Service Workers pane shows a Periodic Sync input where you type your tag and click Periodic Sync to dispatch a periodicsync event immediately.
  4. Add a console.log(event.tag) at the top of each handler and confirm it appears in the service worker console when you fire the tag.

Persisting the data each handler produces should go through a durable store; coordinate the writes with IndexedDB Transaction Management and the broader caching approach in Service Worker Caching Strategies.

Edge cases and the cross-browser fallback

// Portable fallback: drain on reconnect, deduped with a Web Lock.
function installOnlineFallback(): void {
  globalThis.addEventListener('online', () => {
    if (navigator.locks) {
      // ifAvailable: if another context already holds the lock, skip silently.
      navigator.locks.request('outbox-drain', { ifAvailable: true }, async (lock) => {
        if (!lock) return; // another flush is already in progress
        await flushQueueNow();
      });
    } else {
      void flushQueueNow();
    }
  });
}

async function flushQueueNow(): Promise<void> {
  // Drain the IndexedDB outbox over fetch(); shared with the SW handler logic.
}

For a complete reliable one-off sync queue built on these primitives, see Implementing Reliable Background Sync for Form Submissions.

Frequently Asked Questions

When should I use periodic sync instead of one-off sync?

Use one-off sync to deliver a specific queued action once when the device reconnects, such as posting a comment the user wrote offline. Use periodic sync only to keep cached content fresh on a recurring schedule, and only when the app is installed as a PWA and the periodic-background-sync permission is granted. Periodic sync is an optimization, not a delivery guarantee.

Do these APIs work in Safari or Firefox?

No. Both one-off Background Sync and Periodic Background Sync are Chromium-only as of mid-2026. On other engines registration.sync and registration.periodicSync are undefined. Provide a fallback that drains your outbox on the online event so the feature still works everywhere a tab is open.

Why does my periodicsync event never fire even on Chrome?

The browser throttles periodicsync aggressively based on user engagement, battery, and network, and it requires an installed PWA with the permission granted. Your registered minInterval is a floor, not a promise — the event may be delayed far longer or skipped. Verify it manually from the DevTools Application panel rather than waiting.

Related