Skip to content

API in Service Worker

Periodic Background Sync API

Published

Limited availabilityNot supported in Firefox (Desktop), Firefox (Android), Safari (macOS), Safari (iOS)WICG draft

The Periodic Background Sync API lets an installed web app register a named task that the browser runs in the service worker at intervals it chooses, no more often than the minInterval the app asks for, so content is already fresh when the user next opens the app. It shipped in Chrome 80, Edge 80, and Samsung Internet 13, is not implemented in Firefox or Safari (BCD api.PeriodicSyncManager), and Chrome only grants it to apps that are installed and have a site-engagement score above zero.

Flow diagram of periodic background sync: an installed PWA with enough site engagement checks the periodic-background-sync permission and calls periodicSync.register() with a tag and minInterval; the browser schedules the sync no more often than minInterval, fires the periodicsync event in the service worker when the time arrives and the device is online, and the worker fetches and caches new data inside event.waitUntil(). The cycle repeats until the app is uninstalled, engagement decays or periodicSync.unregister() is called.
Periodic background sync: from permission and registration to the repeating periodicsync event.
// Page or service worker
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register(tag, { minInterval });
const tags = await registration.periodicSync.getTags();
await registration.periodicSync.unregister(tag);
// Service worker
self.addEventListener('periodicsync', (event) => {
event.waitUntil(refresh(event.tag));
});

register() returns a Promise<undefined> once the registration is stored; registering an existing tag replaces its minInterval. getTags() resolves with every tag still registered, and unregister() resolves with undefined whether or not the tag existed. The periodicsync event carries only tag; unlike one-off background sync there is no lastChance, because a failed run is simply followed by the next scheduled one.

The method takes a tag and an options object.

Name Type Description
tag string Identifies the registration and is delivered as PeriodicSyncEvent.tag. One registration per tag; several tags can run on different cadences.
options.minInterval number Minimum milliseconds between two runs. The browser treats it as a floor, not a schedule: Chrome aligns the real frequency with how often the app is used and with the device’s power and connectivity state, and it skips a run entirely when the engagement score is zero.

Chrome also requires a previously used network to be present before it fires the event, so a device on a network it has not used before does not sync until the user opens the app once on that network.

register() rejects in three cases (MDN, PeriodicSyncManager.register()).

  • InvalidStateError (DOMException): the registration has no active service worker. Await navigator.serviceWorker.ready first.
  • NotAllowedError (DOMException): the periodic-background-sync permission is not granted. In Chrome this is the state in any ordinary browser tab, because the permission is only granted to an installed app; the same code succeeds once the app is launched from its own window.
  • InvalidAccessError (DOMException): the calling window is not a top-level or auxiliary browsing context, for example an iframe.
  • TypeError: registration.periodicSync is undefined in Firefox and Safari, so the member access itself throws. Feature-detect with 'periodicSync' in registration.

The examples share one service worker at /sw.js and a news feed cached under the name news.

Registering a daily refresh after checking the permission

Section titled “Registering a daily refresh after checking the permission”

The page queries the permission before calling register() so that an uninstalled app does not hit NotAllowedError, and it shows the user a “refresh on open” notice instead.

async function enableDailyRefresh() {
const status = await navigator.permissions.query({ name: 'periodic-background-sync' });
if (status.state !== 'granted') {
showNotice('Install the app to get headlines refreshed in the background.');
return;
}
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register('news-refresh', {
minInterval: 24 * 60 * 60 * 1000,
});
}

Firefox and Safari throw a TypeError from permissions.query() for an unknown permission name, so a cross-browser caller wraps the query in try/catch and treats the failure as “not granted”.

The worker fetches the feed and stores it; a thrown error rejects the waitUntil() promise, which the browser logs and ignores until the next scheduled run.

self.addEventListener('periodicsync', (event) => {
if (event.tag !== 'news-refresh') return;
event.waitUntil((async () => {
const response = await fetch('/api/headlines', { cache: 'no-store' });
if (!response.ok) throw new Error(`headlines: ${response.status}`);
const cache = await caches.open('news');
await cache.put('/api/headlines', response);
})());
});

Keep the work short: Chrome ends the event after its own execution limit, and a worker that is still running then is terminated without the cache write completing.

Detecting support and refreshing on return instead

Section titled “Detecting support and refreshing on return instead”

When periodicSync is missing, or the permission is not granted, the fallback refreshes the feed when the page regains visibility, which covers the “fresh on open” goal for a tab or an uninstalled app at the cost of one visible network wait.

async function setUpRefresh(refreshNow) {
const registration = await navigator.serviceWorker.ready;
if ('periodicSync' in registration) {
try {
await registration.periodicSync.register('news-refresh', { minInterval: 24 * 60 * 60 * 1000 });
return;
} catch {
// Rejected in a plain browser tab: fall through to the foreground fallback.
}
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') refreshNow();
});
refreshNow();
}

The fallback runs only while the page exists; it cannot refresh content for a closed app, which is the gap the API closes.

Specifications

SpecificationStatus
Periodic Background SyncWICG draft
  • Legend
  • Yes
  • Partial
  • Flag
  • No
  • Unknown
Browser / PlatformSupportVersionsConfidenceSourceNotes
Chrome (Desktop)Yes80highsource—
Chrome (Android)Yes80highsource1
Edge (Desktop)Yes80highsource2
Firefox (Desktop)No—highsource3
Firefox (Android)No—highsource45
Safari (macOS)No—highsource6
Safari (iOS)No—highsource78
Samsung InternetYes13.0highsource9
WebView (Android)No—highsource10
  1. Derived by browser-compat-data mirroring from Chrome.
  2. Derived by browser-compat-data mirroring from Chrome.
  3. No Firefox support is recorded in browser-compat-data.
  4. No Firefox for Android support is recorded in browser-compat-data.
  5. Derived by browser-compat-data mirroring from Firefox.
  6. No Safari support is recorded in browser-compat-data.
  7. No Safari on iOS support is recorded in browser-compat-data.
  8. Derived by browser-compat-data mirroring from Safari.
  9. Derived by browser-compat-data mirroring from Chrome Android.
  10. Implementation tracking: https://crbug.com/40151529.

Source data: /compatibility/periodic-background-sync.json · Global usage: 72 % (StatCounter 2026-05)

Source: spec · MDN · Last verified 2026-10-03 · Confidence: high (computed from sources)