How to Test Background Sync on Web (Complete Guide)
Background Sync is a progressive web app feature that lets a service worker defer work until the browser regains connectivity. In theory this improves reliability: a user can submit a form while offli
Why Background Sync Deserves Dedicated Testing
Background Sync is a progressive web app feature that lets a service worker defer work until the browser regains connectivity. In theory this improves reliability: a user can submit a form while offline, and the request will be sent later when the network returns. In practice the API introduces failure modes that are invisible to unit tests and often slip through manual QA because they depend on timing, network state, browser throttling, and user‑initiated events. A missed sync can mean lost orders, abandoned sign‑ups, or stale data that corrupts downstream processes. Because the sync runs outside the page lifecycle, ordinary debugging tools (breakpoints, console.log) do not capture it unless you explicitly inspect the service worker. Consequently, a dedicated test strategy is required to catch regressions before they reach production.
How the Background Sync API Works
Core Concepts
The API consists of two parts: registration of a sync event and handling of that event inside a service worker.
// In a page script
if ('serviceWorker' in navigator && 'SyncManager' in window) {
navigator.serviceWorker.ready.then(reg => {
reg.sync.register('my-tag'); // requests a background sync
});
}
// Inside the service worker (sw.js)
self.addEventListener('sync', event => {
if (event.tag === 'my-tag') {
event.waitUntil(
// Perform the deferred work, e.g., send a POST request
fetch('/api/submit', {method: 'POST', body: JSON.stringify(payload)})
.then(response => {
if (!response.ok) throw new Error('Sync failed');
})
.catch(err => {
// Optionally re‑register for retry
console.warn('Background sync failed', err);
})
);
}
});
Key points to remember for testing:
- The sync callback runs after the page may have been unloaded.
- The browser may delay execution based on power saving, battery level, or user preferences.
- If the sync fails, the browser will automatically retry with exponential back‑off unless you cancel the registration.
- The API is only available on secure contexts (HTTPS or localhost).
Lifecycle Diagram
[Page] --registerSync--> [Service Worker] --(network available)--> [Sync Event]
|
v
[Fetch/XHR] --> [Success/Retry]
Understanding this flow helps you decide where to inject faults and where to observe outcomes.
Test Matrix for Background Sync
Below is a comprehensive matrix that covers the dimensions you should verify. Each cell indicates the expected outcome and the primary technique to validate it.
| Dimension | Scenario | Expected Result | Verification Method |
|---|---|---|---|
| Happy Path | User goes offline, submits form, comes online within 5 s | Sync fires once, request succeeds, UI shows success | Monitor network tab, check service worker logs, assert UI update |
| Offline → Online Delay | Same as above, but network returns after 30 s | Sync fires after delay, request succeeds, no data loss | Use network throttling, advance clock with fake timers |
| Multiple Registrations | User triggers same sync tag twice while offline | Only one sync event queued; duplicate registration ignored | Count sync events in service worker, verify no duplicate requests |
| Sync Failure (5xx) | Server returns 500 on sync attempt | Browser retries with back‑off; after max attempts, event ends | Simulate server error, observe retry pattern in devtools |
| Sync Failure (Network) | Network drops immediately after sync start | Browser retries; eventual success when network restored | Kill network, then restore, check that request finally succeeds |
| Power Saver Mode | Device/battery saver enabled | Sync may be delayed; eventually fires when allowed | Enable battery saver in Chrome devtools, observe delay |
| Background Tab Throttling | Page hidden, sync registered, then tab hidden | Sync still fires (service worker runs independent) | Hide tab, verify sync event still logged |
| Service Worker Update | New SW version installed while sync pending | Sync uses the new SW; old SW terminated | Version SW, check that new SW handles the sync event |
| Accessibility (Screen Reader) | User with screen reader triggers sync | Same behavior; announcements reflect offline/online state | Use axe or manual SR test, verify live region updates |
| Security/Privacy | Sync attempts to send sensitive data over HTTP | Request blocked; CSP or mixed‑content error appears | Attempt sync to http endpoint, verify block in console |
| Permission Revocation | User revokes notifications/sync permission | Sync registration throws NotAllowedError | Deny permission via site settings, catch registration error |
| Quota Exceeded | Payload exceeds IndexedDB storage quota | Sync registration fails with QuotaExceededError | Fill storage, attempt large payload sync, catch error |
| Cross‑Origin Iframe | Sync registered from iframe with different origin | Registration fails unless iframe allowed via CSP | Try sync from sandboxed iframe, check error |
| Long‑Running Sync | Sync performs heavy computation (e.g., crypto) | Browser may abort after timeout; fallback needed | Run heavy work, observe abort, implement chunking |
*Notes*:
- The “Verification Method” column suggests concrete actions you can take in manual or automated tests.
- Some scenarios (e.g., power saver) are environment‑specific; you may need to simulate them via devtools or flags.
Manual Testing Approach
Prerequisites
- Local HTTPS – Use
mkcertorlocalhostto serve your app over a secure origin. - Service Worker Devtools – Open Chrome DevTools → Application → Service Workers. Enable “Update on reload” and “Bypass for network requests”.
- Network Panel – Preserve log, enable throttling (Slow 3G, Offline, Online).
- Console – Filter to “ServiceWorker” to see sync‑related logs.
Step‑by‑Step Procedure
- Register a Listener – Add a temporary
console.loginside the sync handler to tag each execution. - Go Offline – In the Network tab, select “Offline”.
- Trigger Sync – Perform the user action that registers the sync (e.g., click “Save Draft”).
- Verify Registration – In the Application panel, under “Background Sync”, you should see a pending sync with your tag.
- Go Online – Switch network to “Online” (or restore real connection).
- Observe Sync Fire – Check the console for your log; the Network panel should show the outgoing request.
- Validate UI – Confirm that the UI reflects the successful outcome (e.g., toast message, list update).
- Test Failure – While online, use the Network tab to block a specific request (right‑click → Block request domain) or use a tool like
toxiproxyto return 500. - Check Retry – Ensure the browser retries after a back‑off interval (visible as repeated requests).
- Clean Up – Unregister the sync (if you exposed an API) or reload the page to clear pending syncs.
Edge‑Case Checks
- Duplicate Tags – Call
registerSynctwice quickly; verify only one pending entry. - Tab Hidden – After registering sync, switch to another tab; repeat steps 5‑7 to confirm sync still fires.
- Battery Saver – In Chrome, open DevTools → Rendering → Emulate CSS media feature
prefers-reduced-dataor use the “Battery” panel to set low charge; observe any delay. - Permission Revocation – Visit Site Settings → Permissions → Background Sync → Block; try to register sync and catch the promise rejection.
Documentation
Record each step in a test‑case template:
| Test ID | Preconditions | Action | Expected Result | Actual Result | Pass/Fail | Notes |
|---|
Manual testing is valuable for exploratory work, but it does not scale across browsers or CI pipelines. The next section covers automation.
Automated Testing Strategies
Automating background sync tests requires three capabilities:
- Control over network state (online/offline, latency, throttling).
- Ability to inspect service worker events (sync firing, retries).
- Mechanism to assert UI or state changes after the sync completes.
Using Playwright
Playwright provides a robust API to manage service workers and network conditions.
// test/background-sync.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Background Sync', () => {
test('saves form data when offline and sends on reconnect', async ({ page }) => {
// 1. Start from a clean service worker state
await page.context().clearCookies();
await page.goto('https://myapp.example.com/', { waitUntil: 'networkidle' });
// 2. Go offline
await page.context().setOffline(true);
// 3. Fill and submit form (triggers sync registration)
await page.fill('#email', 'user@example.com');
await page.click('#submit');
// 4. Verify pending sync appears
const [worker] = await page.context().serviceWorkers();
await worker.waitForEvent('statechange', e => e.state === 'activated');
const syncs = await worker.evaluate(() =>
navigator.serviceWorker.controller.sync.getTags()
);
expect(syncs).toContain('my-tag');
// 5. Go online
await page.context().setOffline(false);
// 6. Wait for the sync request to appear
await page.waitForResponse(resp =>
resp.url().endsWith('/api/submit') && resp.request().method() === 'POST'
);
// 7. Assert UI update
await expect(page.locator('#status')).toHaveText('Saved');
});
});
Explanation
setOffline(true/false)toggles the network for the whole browser context.context().serviceWorkers()returns the active service worker; you can listen to itsstatechangeand inspect the sync manager.waitForResponseensures the test does not proceed until the deferred request is made.
Using Puppeteer with Fake Timers
If you need to test long delays without waiting real time, you can mock the timer API inside the service worker.
// sw.js (test build)
self.addEventListener('sync', async event => {
if (event.tag === 'my-tag') {
// Use a configurable delay for testing
const delay = __TEST_DELAY__ || 0;
await new Promise(res => setTimeout(res, delay));
await fetch('/api/submit', {method: 'POST'});
}
});
In the test:
await page.evaluateOnNewDocument(() => {
window.__TEST_DELAY__ = 5000; // 5 seconds
});
await page.context().setOffline(true);
// trigger sync …
await page.context().setOffline(false);
// Fast‑forward timers
await page.waitForTimeout(5100); // or use page.evaluate(() => new Promise(r => setTimeout(r, 0)));
Using Workbox Testing Utilities
If your app uses Workbox, you can leverage its testing module to simulate sync events directly:
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
import { BackgroundSyncPlugin } from 'workbox-background-sync';
const bgSyncPlugin = new BackgroundSyncPlugin('my-tag', {
maxRetentionTime: 24 * 60, // minutes
});
registerRoute(
({url}) => url.pathname.startsWith('/api/submit'),
new NetworkOnly({ plugins: [bgSyncPlugin] })
);
In a test you can call:
await navigator.serviceWorker.ready.then(reg =>
reg.sync.getTags().then(tags => {
if (!tags.includes('my-tag')) return reg.sync.register('my-tag');
})
);
Then assert that the plugin’s internal queue length changes as expected.
Assertions Beyond Network
Sometimes the sync performs work that does not involve a network request (e.g., updating IndexedDB). In those cases, you can expose a debugging port in the service worker:
// sw.js
self.addEventListener('message', event => {
if (event.data === 'dump-db') {
idb.open('my-db', 1).then(db => {
db.transaction('store').objectStore('store').getAll()
.then(vals => self.clients.matchAll().then(clients =>
clients[0].postMessage({type: 'db-dump', payload: vals}))
);
});
}
});
Your test then sends a message and checks the response.
CI Integration
- Install Playwright (
npm i -D @playwright/test). - Add a script:
"test:sync": "playwright test --project=chromium". - In CI, set
--timeouthigh enough to accommodate simulated delays. - Capture Playwright traces (
--trace on) for debugging flaky`) to inspect service worker logs.
Limitations
- Browser vendors may throttle background sync in headless mode; Chrome’s
--disable-background-timer-throttlingflag can mitigate this. - Some privacy settings (e.g., “Block background sync”) are not exposed via automation; you must rely on manual checks for those.
Leveraging Autonomous, Persona‑Driven Exploration
Manual scripts excel at verifying known flows, but they often miss edge cases that arise from unexpected user behavior or environmental quirks. Autonomous QA platforms—like SUSA—address this gap by exploring the application with a variety of simulated personas, each embodying distinct interaction patterns, network tolerances, and accessibility needs.
How SUSA Works
- Ingestion – You upload an APK (for Android WebView) or point SUSA at a web URL.
- Persona Engine – Eight built‑in personas (curious, impatient, novice, adversarial, elderly, accessibility, power user, and privacy‑conscious) drive the explorer. Each persona has a configurable profile: tap speed, scroll depth, form‑field tolerance, willingness to grant permissions, and simulated network conditions.
- Exploration Loop – The agent loads the page, registers any service workers it finds, and begins interacting. It records every network request, service‑worker event, and DOM mutation.
- Fault Injection – While exploring, SUSA intermittently flips the network to offline, simulates battery‑saver mode, or throttles the CPU to mimic low‑end devices.
- Verdict Generation – For each discovered flow (login, checkout, etc.) the platform assigns PASS/FAIL based on observable criteria: did a background sync eventually fire? Was data persisted? Did the UI reflect the correct state?
- Regression Script Generation – After a run, SUSA emits ready‑to‑run Appium (Android) and Playwright (Web) scripts that reproduce the exact sequences that led to failures, enabling rapid regression testing in CI.
Why Personas Find Sync Bugs Scripts Miss
| Persona | Typical Behavior | Sync‑Related Blind Spot It Can Reveal |
|---|---|---|
| Impatient | Rapid taps, quick navigation away before the sync registers | Registers sync but navigates off before the service worker finishes setup, causing the registration to be dropped. |
| Elderly | Slow inputs, long dwell times on fields | Triggers sync after a prolonged offline period; tests the maximum retention time of the BackgroundSyncPlugin. |
| Accessibility | Relies on screen reader, uses keyboard navigation exclusively | May open a modal that intercepts the sync‑registering button, preventing the registration from firing; verifies that sync works regardless of focus modality. |
| Adversarial | Attempts to submit malformed data, revokes permissions mid‑flow | Tests error paths: sync registration throws NotAllowedError when permissions are revoked after the user starts a flow. |
| Power User | Opens many tabs, uses devtools, enables experimental flags | Checks that sync behaves correctly when multiple service workers coexist (e.g., from different origins) and that the correct SW handles the event. |
| Privacy‑Conscious | Frequently clears site data, disables background sync via settings | Confirms that the app gracefully handles missing sync registration and falls back to immediate transmission or user notification. |
Practical Example: Detecting a Race Condition
Suppose your app registers a sync inside a click handler that also navigates to a confirmation page. A scripted test that waits for navigation to complete before checking the sync may never see the race. An impatient persona, however, may trigger the navigation before the click handler finishes, leaving the sync unregistered. SUSA would log:
- Click event timestamp
- Navigation start timestamp
- Service‑worker registration attempt (failed because the page was already unloading)
From this, you can infer that the sync registration must be decoupled from navigation or delayed until after the pagehide event.
Setting Up SUSA for Your Project
# Install the agent
pip install susatest-agent
# Run against a staging URL
susatest run --url https://staging.myapp.com --personas all --output ./susareport
The command produces a JSON report with a backgroundSync section listing each observed sync event, its tag, outcome, and any associated errors. You can plug this report into your CI as a gate: if any sync‑related FAIL appears, the build fails.
Complementing Scripted Tests
- Use scripted tests for deterministic verification (e.g., “sync fires after 5 seconds online”).
- Use SUSA for exploratory verification (e.g., “does any combination of persona behavior and network throttling break sync?”).
Together they give you confidence that both the happy path and the hidden edge cases are covered.
Edge Cases That Only Surface in Production
Even with thorough lab testing, certain conditions only manifest when real users interact with the app under uncontrolled circumstances. Below are the most common production‑only sync pitfalls and how to mitigate them.
1. Variable Network Recovery Times
In the lab you might simulate a 10‑second outage, but in the wild a user could experience a minutes‑long loss (e.g., entering a subway). If your sync registration has a short maxRetentionTime (default is 5 minutes in Chrome), the sync may be dropped before the connection returns.
Mitigation
- Configure the
BackgroundSyncPluginwith a sufficiently largemaxRetentionTime(e.g., 2 hours) for critical actions. - Provide a UI fallback: show a banner that informs the user the action will be sent when connectivity returns, and allow manual retry.
2. Battery‑Saver and Low‑Power Modes
Android’s Battery Saver and iOS’s Low Power Mode can defer background work indefinitely until the device is charging or the user whitelists the app. Chrome respects these signals and may delay sync beyond the expected window.
Mitigation
- Listen to the
powerchangeevent (via the Battery Status API, where available) and, if the battery is critical, prompt the user to plug in or defer non‑essential syncs. - Log sync delays to your analytics so you can detect patterns tied to power state.
3. Storage Pressure
Background sync often relies on IndexedDB to persist the payload. If the device is low on storage, the write may fail with a QuotaExceededError, causing the sync to be silently dropped.
Mitigation
- Wrap IndexedDB writes in a try/catch and, on quota error, inform the user to free space or switch to a lighter mode (e.g., upload only metadata).
- Monitor
navigator.storage.estimate()and trigger a cleanup routine when usage exceeds a threshold.
4. Multiple Service Worker Versions
When you deploy a new version of your service worker, the old version may still be controlling some tabs. If the new version changes the sync tag or handler, pending syncs from the old version may never be resolved.
Mitigation
- Increment the sync tag whenever you change the handler logic (e.g.,
my-tag-v2). - In the
activatelistener, callawait clients.claim()to take control of existing pages quickly.
5. Cross‑Origin Isolation and COOP/COEP
New security policies (Cross‑Origin Opener Policy, Cross‑Origin Embedder Policy) can block a service worker from accessing certain APIs if the page is not properly isolated. This can lead to silent failures where the sync registration appears to succeed but the event never fires.
Mitigation
- Serve your app with
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corp. - Test in Chrome’s “Site Isolation” devtools flag to verify behavior.
6. User‑Initiated Permission Revocation Mid‑Flow
A privacy‑conscious user might open site settings and toggle off background sync while your app is still offline. The next time the network returns, the sync will be silently dropped.
Mitigation
- Periodically check
navigator.permissions.query({name: 'background-sync'})and, if the state isdenied, show a warning that offline actions may not be sent. - Provide an explicit “Send now” button that attempts an immediate fetch, bypassing sync when permission is denied.
7. Vendor‑Specific Quirks
Safari does not implement the Background Sync API at all (as of 2024). Firefox has it behind a flag. If you rely on sync for critical fallback, you must detect support and provide an alternative (e.g., queue requests in IndexedDB and retry on online event).
Mitigation
- Feature‑detect:
if ('SyncManager' in window) { … } else { /* fallback */ }. - Log a warning to your error‑tracking service when sync is unsupported, prompting a product decision.
Accessibility and Security Considerations
Accessibility
Background sync itself is invisible to assistive technologies, but the user‑visible consequences (toasts, status updates, form state) must be accessible.
- Live Regions – When a sync completes successfully or fails, update an
aria-live="polite"region so screen readers announce the outcome. - Focus Management – If a sync error requires user interaction (e.g., “Please check your connection”), ensure focus moves to the relevant message or action button.
- Reduced Motion – Avoid using animations that could trigger vestibular issues when indicating sync status changes.
You can test these with axe-core or the built‑in Accessibility tab in Chrome DevTools, combined with a screen reader (NVDA, VoiceOver).
Security
Because the sync runs in a service worker context, it inherits the same origin and CSP restrictions as the page. However, a few nuances deserve attention:
- Token Exposure – If you store auth tokens in IndexedDB for later sync, ensure they are encrypted at rest (using SubtleCrypto with a key derived from a user‑only secret).
- Replay Attacks – An attacker who captures a sync request could replay it. Mitigate by including a nonce or timestamp that the server validates and rejects if too old.
- CSRF – Sync requests are same‑origin by default, but if you ever allow cross‑origin endpoints, enforce SameSite cookies and double‑submit CSRF tokens.
- Permission Leakage – Do not request the background-sync permission unless you truly need it; unnecessary permissions increase the attack surface.
Run a security scanner (e.g., OWASP ZAP) against your service worker URL to verify that no unintended endpoints are exposed.
Consolidated Checklist
Use this list before each release to verify that your background‑sync implementation is robust.
| ✅ Item | Description | How to Verify |
|---|---|---|
| Feature Detection | Code guards sync usage with if ('SyncManager' in window). | Search source for unguarded navigator.serviceWorker.ready.sync. |
| Correct Tagging | Each distinct workflow uses a unique sync tag; tags are versioned when handler logic changes. | Review sync registration calls; confirm tag includes a version suffix if applicable. |
| Sufficient Retention | maxRetentionTime set to cover worst‑case expected offline period (e.g., 2 h for critical actions). | Inspect BackgroundSyncPlugin options or raw sync.register call. |
| Error Handling | Sync failure paths catch exceptions, optionally re‑register, and surface UI feedback. | Add try/catch around event.waitUntil; test with mocked 500 responses. |
| UI Feedback | Success/failure states are reflected in UI and announced via live regions or toast. | Manual test + axe check for aria-live updates. |
| Network Throttling | App behaves correctly under Slow 3G, LTE, and offline→online transitions. | Use DevTools throttling or Playwright setOffline/setNetworkConditions. |
| Battery Saver Simulation | Sync still fires (maybe delayed) when battery saver is on. | Chrome DevTools → Rendering → Emulate CSS media feature prefers-reduced-data or use Battery panel. |
| Storage Quota Test | Sync handles QuotaExceededError gracefully. | Fill IndexedDB to near quota, trigger sync, observe error handling. |
| Permission Revocation | App detects denied permission and informs user. | Deny background‑sync permission in site settings, attempt sync, check UI. |
| Multiple SW Versions | Old SW does not block new syncs; clients.claim() called in activate. | Deploy two SW versions, verify pending syncs from old SW are cleared or migrated. |
| Cross‑Origin Checks | No sync registration from sandboxed iframes without proper CSP. | Attempt registration from iframe, expect NotAllowedError. |
| Accessibility | Status messages use aria-live and focus management. | Run axe, test with screen reader. |
| Security | Tokens encrypted, nonces used, CSP headers present. | Review code, run ZAP scan on service worker scope. |
| Fallback for Unsupported Browsers | When SyncManager missing, app queues requests and retries on online event. | Disable sync flag in Firefox, verify fallback works. |
| Test Coverage | At least one automated test covers happy path, one error path, and one edge case (e.g., quota). | Review test suite; ensure background-sync.spec.js includes these scenarios. |
| Monitoring | Production logs capture sync start, end, and retry counts. | Verify that your logging service receives sync-start and sync-end events with correlation IDs. |
| Documentation | Run‑book outlines steps to troubleshoot missing sync (check service worker, permissions, storage). | Confirm internal wiki/page exists and is up‑to‑date. |
If any item fails, treat it as a blocker for release.
Closing Takeaways
Background Sync is a powerful tool for making web applications resilient to flaky connections, but its asynchronous, service‑worker‑bound nature creates a class of bugs that evade traditional unit and integration tests. A solid testing strategy therefore combines:
- Deterministic automated checks (Playwright/Puppeteer) that verify the happy path, known error paths, and configurable timing scenarios.
- Exploratory, persona‑driven runs (using platforms like SUSA) that surface unexpected interactions—such as impatient navigation, permission revocations mid‑flow, or storage pressure—by exercising the app through varied behavioral models and simulated environmental constraints.
- Targeted manual validation for accessibility, security, and platform‑specific quirks that are difficult to encode in scripts (e.g., Battery Saver behavior, CSP interactions).
By maintaining a living test matrix, instrumenting your service worker with observable logs, and treating background sync as a first‑class citizen in your CI pipeline, you can catch regressions before they erode user trust. Remember that the goal is not merely to confirm that a sync *can* happen, but to guarantee that it *will* happen—or fail gracefully—under the real‑world conditions your users encounter.
---
*This guide is intended for developers and QA engineers who need to ship reliable offline‑first experiences. Apply the patterns, adapt the matrix to your feature set, and let both scripted and autonomous testing keep your background sync robust.*
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free