How Safari Service Workers Enable Offline Caching and Sync

This article explains how Service Workers function within the Safari web browser to intercept network requests, manage offline caching, and handle background data synchronization. By acting as a client-side proxy between the web application and the network, Service Workers allow developers to build resilient, offline-first web applications in Apple's WebKit engine. Below, we cover the registration lifecycle, the mechanics of network interception via the Fetch API, Cache API storage strategies, and the specific workarounds required to achieve background sync in Safari.

Registration and Lifecycle

Before a Service Worker can intercept requests in Safari, it must be registered by the main JavaScript thread. Because Service Workers can hijack network connections, Safari strictly requires the web application to be served over HTTPS (with the exception of localhost for development).

  1. Registration: The web app calls navigator.serviceWorker.register('/sw.js').
  2. Installation: Safari downloads the script and triggers the install event in the background. This is where developers typically pre-fetch and cache static assets.
  3. Activation: Once installed, the Service Worker triggers the activate event. Upon activation, the Service Worker takes control of all pages within its defined scope, positioning itself between Safari and the network.

Intercepting Network Requests

An active Service Worker acts as a programmable network proxy. Every time the Safari browser attempts to fetch a resource—whether it is an HTML document, a CSS file, an image, or a JSON payload from an API—the request passes through the Service Worker first.

This interception is handled via the fetch event listener inside the Service Worker script:

  • Listening: The Service Worker listens for the fetch event, which fires every time a network request is made by the controlled page.
  • Hijacking: Inside the event listener, the method event.respondWith() is called. This explicitly tells Safari to halt its default network request behavior and wait for the Service Worker to provide a custom Response object, which can be generated dynamically, fetched from the network, or pulled from local storage.

Enabling Offline Caching

Offline capabilities rely on combining fetch interception with the browser's Cache API. Unlike standard HTTP browser cache, the Cache API is fully programmable and persists until explicitly deleted by the script or evicted by Safari.

Developers generally use two phases to enable offline access:

  • Pre-caching (Install Phase): During the install event, the Service Worker opens a named cache using caches.open() and downloads essential app shell files (HTML, CSS, core JS) using cache.addAll(). event.waitUntil() is used to ensure the Service Worker does not finish installing until all files are safely cached.
  • Runtime Intercept (Fetch Phase): When the user navigates the app offline, the fetch event intercepts the request. The Service Worker calls caches.match(event.request) to check if the requested file exists in the Cache API.
  • Serving the Response: If a match is found, the cached response is passed into event.respondWith(), loading the page instantly without a network connection. If no match is found, the Service Worker attempts a real network fetch(). If the network fails, it catches the error and returns a custom offline fallback page from the cache.

Note on Safari Storage: Safari utilizes Intelligent Tracking Prevention (ITP). If a user does not interact with a standard website for roughly seven days, Safari may automatically purge the Service Worker's Cache API storage to save space and protect privacy. However, if the user adds the website to their iOS or macOS Home Screen as a Progressive Web App (PWA), this seven-day eviction limit is largely lifted.

Background Sync in Safari

Background synchronization is designed to defer data transmission (like submitting a form or uploading a message) until the user has a stable internet connection.

The Standard Approach vs. Safari's Reality In Chromium-based browsers, a failed network request is stored in IndexedDB, and the app registers a sync task via registration.sync.register(). When connectivity returns, the browser wakes the Service Worker and fires a sync event to send the data, even if the user closed the tab.

However, Safari (WebKit) does not natively support the Background Sync API due to stringent battery life and user privacy constraints. Safari will not wake up a Service Worker in the background if the browser or web app is completely closed.

How to Implement Sync in Safari To achieve reliable data synchronization in Safari, developers must combine Service Worker request interception with IndexedDB and foreground event listeners:

  1. Intercept and Store: When the user submits data (POST or PUT requests) while offline, the Service Worker intercepts the request during the fetch event. Upon detecting a network failure in the .catch() block, the Service Worker extracts the request payload and saves it into an "outbox" store inside Safari's IndexedDB.
  2. Return a Mock Response: The Service Worker returns a custom 200/202 HTTP response to the main UI thread, letting the application know the data has been safely queued for later.
  3. Trigger Sync on Reconnection: Because Safari will not fire a background sync event, the main application thread must listen for the browser's window.addEventListener('online') event. When the device regains connectivity while the app is open, the main thread sends a message to the Service Worker via postMessage().
  4. Flush the Queue on App Launch: If the user closes the Safari tab while offline and reopens it later when online, the Service Worker will wake up to intercept the initial page load fetch request. During this wake-up cycle, or during the Service Worker's activate event, the script checks IndexedDB for any queued requests, replays them over the network, and deletes them from the database upon success.