10LOC
Chrome APIsadvanced

Periodic Background Sync for resilient refresh jobs

Published August 24, 2027

type PeriodicSyncRegistration = ServiceWorkerRegistration & {
  periodicSync: { register: (tag: string, options: { minInterval: number }) => Promise<void> };
};

export const schedulePeriodicRefresh = async (tag: string, minIntervalMs: number): Promise<boolean> => {
  const registration = await navigator.serviceWorker.ready;
  if (!("periodicSync" in registration)) return false;
  const status = await navigator.permissions.query({ name: "periodic-background-sync" } as unknown as PermissionDescriptor);
  if (status.state !== "granted") return false;
  await (registration as PeriodicSyncRegistration).periodicSync.register(tag, { minInterval: minIntervalMs });
  return true;
};

What

registration.periodicSync.register(tag, { minInterval }) asks the browser to periodically wake the service worker and fire a periodicsync event — roughly every minInterval milliseconds, subject entirely to the browser's own scheduling — so an installed PWA can refresh cached content without the user having the app open.

Why it matters

A news app, a mail client, or anything that wants "fresh content the moment you open it" normally has to fetch on launch and eat the loading spinner, or run a setInterval that only exists while a tab is open (and stops the second it's closed). Periodic Background Sync lets an installed PWA prefetch content on the browser's own schedule, in the background, so content is already cached by the time the user opens the app — the same experience native apps get from OS-level background refresh. It's explicitly not a precise timer: minInterval is a floor, not a target, and the browser is free to run it less often — or never — based on signals like battery, network type, and how often the user actually opens the app.

How it works

  • PeriodicSyncRegistration extends ServiceWorkerRegistration with the periodicSync property, which — like one-time Background Sync — is missing from TypeScript's DOM types entirely; the cast is applied only after the feature-detection check.
  • schedulePeriodicRefresh checks "periodicSync" in registration first, so it's a no-op (returns false) on any browser without the API rather than throwing.
  • navigator.permissions.query({ name: "periodic-background-sync" }) checks the current permission state. "periodic-background-sync" isn't part of TypeScript's built-in PermissionName union (it's non-standard), so a direct as PermissionName cast fails with "insufficient overlap" — going through as unknown as PermissionDescriptor first is the correct, narrow way to assert a literal TypeScript's ambient types don't know about, without dropping to a broad any.
  • Only when the status is already "granted" does the function actually call periodicSync.register — there's no explicit "request permission" step to call here, because this permission isn't requestable on demand (see Gotchas).

Gotchas

  • There is no prompt to grant this permission. Chrome decides automatically using a site engagement score computed from how often the user actually visits and uses the installed app — a freshly installed, rarely-opened PWA will see status.state stay "prompt"/"denied" indefinitely, with no dialog for the user (or your code) to trigger.
  • Requires an installed PWA. The API doesn't exist in a regular browser tab at all — "periodicSync" in registration is false for any site the user hasn't installed.
  • Chromium-only, and narrower than one-time Background Sync: Chrome and Edge on desktop and Android; no Firefox or Safari support, and periodic sync is explicitly called out by spec authors as non-standard and not on a standards track — treat it as an enhancement for engaged users, never a guaranteed delivery mechanism.
  • minInterval is a minimum request, not a contract — actual firing frequency degrades based on engagement, battery state, and connectivity, and can silently stop altogether if engagement drops.
  • Unregister with registration.periodicSync.unregister(tag) when the feature is no longer needed — an orphaned registration keeps waking the service worker for content nothing is using anymore.

Related

  • Background Sync — the one-shot sibling for "retry this specific failed action," versus this API's recurring "refresh content on a schedule."
  • Push API — for server-initiated updates instead of client-scheduled polling; a better fit when you need updates faster than a periodic sync interval can promise.