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.
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:
- Open DevTools → Application → Service Workers. Confirm the worker is activated and running.
- 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.
- 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
periodicsyncevent immediately. - 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
- Neither API exists in Firefox or Safari. As of mid-2026, both one-off and periodic Background Sync are Chromium-only. On every other engine
reg.syncandreg.periodicSyncare simplyundefined, so feature-detect and degrade — never assume the event will fire. - Periodic sync may never fire. Even on Chromium, the browser throttles
periodicsyncby engagement, battery, and network, and uninstalling the PWA or revoking the permission stops it entirely. Treat it as an optimization that freshens an already-working cache, not as a guaranteed delivery mechanism. - Online-event fallback. The portable substitute is a
window.addEventListener('online', ...)handler that drains your outbox whenever connectivity returns. It only runs while a tab is open, which is the trade-off for working everywhere. - Deduplicate with Web Locks. If both the
onlinehandler and a one-offsyncevent can fire close together, two flushes can race and double-submit. Wrap the drain in a Web Lock so only one runs at a time, the pattern detailed in Preventing Duplicate Sync with Web Locks.
// 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
- Background Sync API Implementation — the parent guide on deferred execution with service workers.
- Implementing Reliable Background Sync for Form Submissions — a complete one-off sync queue for offline forms.
- Preventing Duplicate Sync with Web Locks — deduplicate concurrent flushes across tabs and events.
- Service Worker Caching Strategies — what periodic sync refreshes and how the cache is structured.
- Offline Sync Strategies & Background Workflows — the broader offline-first architecture these APIs slot into.