# Periodic Background Sync API

> How PeriodicSyncManager.register() asks the browser to fire a periodicsync event in an installed PWA's service worker no more often than minInterval, why Chrome gates it on installation and site engagement, and the exceptions and detection pattern.

import Figure from '@components/Figure.astro';
import periodicSyncDiagram from '@assets/diagrams/periodic-background-sync.svg';

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.

<Figure src={periodicSyncDiagram} alt="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." caption="Periodic background sync: from permission and registration to the repeating periodicsync event." />

## Syntax

```js
// 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.

## Parameters

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.

## Exceptions

`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`.

:::observed
In Chrome DevTools (English UI), the Application panel's Service workers pane has a text field labelled **Periodic Sync** next to the **Sync** and **Push** fields; typing a tag and pressing the adjacent button dispatches a `periodicsync` event with that tag to the selected worker without waiting for the browser's scheduler, and the worker's `console.log()` output appears in the Console with the worker script URL selected in the context dropdown. A separate **Periodic Background Sync** section under Background services records real dispatches after **Start recording** is clicked and keeps recording for up to three days (developer.chrome.com, "Periodic Background Sync" and "Debug Progressive Web Apps").
:::

## Examples

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

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.

```js
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".

### Handling the event in the service worker

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.

```js
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

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.

```js
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.

## See also

- [Web Periodic Background Synchronization: register() method](https://wicg.github.io/periodic-background-sync/#dom-periodicsyncmanager-register) (wicg.github.io)
- [Richer offline experiences with the Periodic Background Sync API](https://developer.chrome.com/docs/capabilities/periodic-background-sync) (developer.chrome.com)
- [Periodic Background Sync browser support](/compatibility/periodic-background-sync/)
- [Background Sync API](/reference/service-worker/background-sync/)
- [Periodic background sync for content updates](/reference/notifications/periodic-background-sync/)
- [Service worker lifecycle](/reference/service-worker/lifecycle/)