Skip to content

Background Sync — Deferring Actions Until Connectivity Returns

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Background Sync. We cover key concepts, practical examples, and best practices to help you master this topic.

Background Sync defers server requests (like form submissions) until the user has connectivity, enabling true offline-first functionality in Progressive Web Apps.

What You'll Learn

By the end of this tutorial, you will understand how Background Sync works, how to register sync events, how to handle them in the service worker, and how to handle sync failures with retry logic.

Why It Matters

Even with precaching and dynamic Caching, users still need to send data to your server — form submissions, comments, likes, purchases. Without Background Sync, these actions fail silently when offline. With it, actions are queued and delivered automatically when connectivity returns.

Real-World Use

A field service PWA lets technicians submit repair reports while working in basements with no signal. The report is stored locally and synced when they walk upstairs and regain connectivity. The technician never loses data and never sees a "network error" message.

Background Sync Architecture

Background Sync Flow
    User submits form while offline
         ↓
    Page detects offline or fetch fails
         ↓
    Register a sync event with a tag
         ↓
    Browser stores the sync request
         ↓
    ... (user goes offline, then online) ...
         ↓
    Browser detects connectivity
         ↓
    Fires 'sync' event in service worker
         ↓
    Service worker sends queued data to server
         ↓
    ┌──────────┐             ┌──────────────┐
    │ Success  │             │  Failure     │
    └────┬─────┘             └──────┬───────┘
         ↓                          ↓
    Notify page              Retry (up to max)
    of success               or show failure

Think of Background Sync like a mail sorting office. You drop your letter in the mailbox (submit form). If the post office is closed (offline), your letter waits in the sorting room. When the post office opens (online), the letter is processed and sent. If delivery fails, it tries again later.

Checking Background Sync Support

// Check if Background Sync is supported
function isBackgroundSyncSupported() {
    return 'serviceWorker' in navigator &&
           'SyncManager' in window;
}

console.log('Background Sync supported:', isBackgroundSyncSupported());

Output:

Background Sync supported: true

Registering a Sync from the Page

async function submitFormOffline(formData) {
    // Store the form data in IndexedDB
    await savePendingAction({
        type: 'FORM_SUBMIT',
        data: formData,
        timestamp: Date.now(),
        id: generateId()
    });

    // Register a sync event
    if (isBackgroundSyncSupported()) {
        try {
            const registration = await navigator.serviceWorker.ready;
            await registration.sync.register('sync-forms');
            console.log('Background sync registered');
        } catch (error) {
            console.log('Sync registration failed, trying manual fallback');
            // Fallback: try sending now
            try {
                await sendFormData(formData);
            } catch (e) {
                console.log('Cannot send now, data saved for later');
            }
        }
    } else {
        // No background sync support
        // Use a polling fallback
        startPollingForSync();
    }
}

// Save pending action to IndexedDB
function savePendingAction(action) {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open('SyncDB', 1);

        request.onupgradeneeded = (event) => {
            const db = event.target.result;
            db.createObjectStore('pending', {
                keyPath: 'id',
                autoIncrement: true
            });
        };

        request.onsuccess = (event) => {
            const db = event.target.result;
            const transaction = db.transaction(['pending'], 'readwrite');
            const store = transaction.objectStore('pending');
            store.add(action);
            resolve();
        };
    });
}

Handling Sync in the Service Worker

// sw.js — Handle sync events
self.addEventListener('sync', event => {
    console.log('Sync event:', event.tag);

    if (event.tag === 'sync-forms') {
        event.waitUntil(processPendingForms());
    } else if (event.tag === 'sync-comments') {
        event.waitUntil(processPendingComments());
    } else if (event.tag === 'sync-analytics') {
        event.waitUntil(processPendingAnalytics());
    }
});

async function processPendingForms() {
    try {
        const actions = await getPendingActions('FORM_SUBMIT');
        console.log(`Processing ${actions.length} pending forms`);

        for (const action of actions) {
            try {
                const response = await fetch('/api/submit-form', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-Sync-Id': action.id
                    },
                    body: JSON.stringify(action.data)
                });

                if (response.ok) {
                    await removePendingAction(action.id);
                    console.log('Synced form:', action.id);

                    // Notify the page
                    const clients = await self.clients.matchAll();
                    clients.forEach(client => {
                        client.postMessage({
                            type: 'SYNC_SUCCESS',
                            actionId: action.id
                        });
                    });
                }
            } catch (error) {
                console.log('Failed to sync form:', action.id, error.message);
                // Will retry on next sync event
            }
        }
    } catch (error) {
        console.error('Error processing pending forms:', error);
    }
}

Retry Logic with Exponential Backoff

async function getPendingActions(type) {
    // Fetch from IndexedDB
    return new Promise((resolve) => {
        const request = indexedDB.open('SyncDB', 1);

        request.onsuccess = (event) => {
            const db = event.target.result;
            const transaction = db.transaction(['pending'], 'readonly');
            const store = transaction.objectStore('pending');
            const all = store.getAll();

            all.onsuccess = () => {
                const filtered = all.result.filter(a => a.type === type);
                resolve(filtered);
            };
        };
    });
}

async function removePendingAction(id) {
    return new Promise((resolve) => {
        const request = indexedDB.open('SyncDB', 1);

        request.onsuccess = (event) => {
            const db = event.target.result;
            const transaction = db.transaction(['pending'], 'readwrite');
            const store = transaction.objectStore('pending');
            store.delete(id);
            resolve();
        };
    });
}

Fallback When Background Sync Is Not Supported

// Fallback: polling sync
let syncInterval = null;

function startPollingForSync() {
    if (syncInterval) return;

    syncInterval = setInterval(async () => {
        if (navigator.onLine) {
            console.log('Polling: attempting to sync pending actions');
            try {
                // Trigger sync via message to service worker
                const registration = await navigator.serviceWorker.ready;
                registration.active.postMessage({
                    type: 'MANUAL_SYNC'
                });
            } catch (error) {
                console.log('Polling sync failed:', error);
            }
        }
    }, 30000); // Every 30 seconds
}

// Listen for manual sync trigger in SW
self.addEventListener('message', event => {
    if (event.data.type === 'MANUAL_SYNC') {
        event.waitUntil(processPendingForms());
    }
});

Sync Status UI

// Show sync status to the user
navigator.serviceWorker.addEventListener('message', event => {
    if (event.data.type === 'SYNC_SUCCESS') {
        showToast('Form submitted successfully!');
        updatePendingCount(-1);
    } else if (event.data.type === 'SYNC_FAILED') {
        showToast('Sync failed, will retry automatically');
    }
});

function showPendingCount() {
    // Count pending items from IndexedDB
    const request = indexedDB.open('SyncDB', 1);
    request.onsuccess = (event) => {
        const db = event.target.result;
        const transaction = db.transaction(['pending'], 'readonly');
        const store = transaction.objectStore('pending');
        const count = store.count();

        count.onsuccess = () => {
            const badge = document.getElementById('pending-count');
            if (badge) {
                badge.textContent = count.result > 0
                    ? `${count.result} pending`
                    : '';
            }
        };
    };
}

window.addEventListener('online', () => {
    console.log('Back online, triggering sync');
    showToast('Back online! Syncing your data...');
    // Trigger sync
    navigator.serviceWorker.ready.then(reg => {
        reg.sync.register('sync-forms');
    });
});

Common Mistakes

  1. Not storing action data persistently. Sync events may fire after the page is closed. Store pending actions in IndexedDB, not in memory.
  2. Assuming sync always succeeds. Network conditions vary. Implement retry logic with exponential backoff and max retry limits.
  3. Registering duplicate sync events. Multiple sync registrations with the same tag are coalesced. The browser fires one sync for the tag.
  4. Not handling sync failures gracefully. Data stuck in pending queue with no user feedback. Sync silently failing erodes trust.
  5. Relying on sync for real-time operations. Background Sync has no latency guarantee. It may fire seconds, minutes, or hours later.

Practice Questions

  1. How does Background Sync differ from a regular fetch request?
  2. What happens if the sync event handler throws an error?
  3. Why should you store pending actions in IndexedDB rather than memory?
  4. How do you implement a fallback when Background Sync is not supported?
  5. How do you show sync progress to the user?

Challenge: Build a complete offline form submission system: save form data to IndexedDB, register a background sync, Process the sync in the service worker with retry logic (max 3 retries, 60-second backoff), and show a status badge indicating pending items.

FAQ

How long does the browser wait before firing a sync event?

The browser fires the sync event when it detects connectivity, typically within seconds of the connection being restored. There is no guaranteed timeframe.

Can Background Sync wake up a closed browser?

On Android, the browser can fire sync events even if the browser is closed. On desktop, the browser must be running but can be backgrounded.

What is the maximum number of sync registrations?

Chrome allows up to 256 sync registrations per origin. Use meaningful tags and avoid registering duplicates.

Does Background Sync work on iOS?

No. Safari does not support Background Sync. Implement a polling fallback for iOS users.

Can I prioritize certain sync events?

Sort your pending actions by priority before processing. Process time-sensitive actions (purchases) before analytics events.

Mini Project

Build a customer feedback form that works offline: when the user submits while offline, save the feedback to IndexedDB and register a background sync. The service worker processes pending feedback when online, with retry logic (max 5 retries, doubling delay 1s, 2s, 4s, 8s, 16s). Show a sync status indicator and display "Thank you" when successfully synced.

What's Next

You can sync data on connectivity change. Now learn Periodic Background Sync for updating content on a schedule without user interaction.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro