Service Worker File Caching for Offline Transfer
Use Service Workers to enable offline file transfer capabilities. Cache strategies, background sync, and progressive web app patterns.
Service Workers let a file transfer app keep working when the network drops: cache the HTML shell and JS with the Cache API, queue failed uploads with the Background Sync API, store partial chunks in IndexedDB, and replay everything when connectivity returns. The worker runs on a separate thread with its own event loop, intercepts fetch events for its scope, and persists through tab closes. For upload tools, the right recipe is a stale-while-revalidate strategy for the app shell, IndexedDB-backed chunk queuing for in-flight transfers, and a periodic Background Sync registration that retries uploads every 15 minutes until they succeed.
What Service Workers Actually Do for Transfer Apps
The big win is that navigator.serviceWorker survives tab reloads, offline periods, and even phone sleep. When a user starts a 2 GB upload on a flaky train Wi-Fi, you want the chunks that already succeeded to stay succeeded, the ones in-flight to retry on reconnection, and the whole state to be recoverable if the browser kills the tab to reclaim memory. A Service Worker — which runs independently of any specific tab — is the piece that enables all of that.
The API gives you three building blocks: Cache for storing responses by URL, IndexedDB for structured data (chunk queues, session state), and SyncManager for scheduling retries that fire when the device is online.
Registering and Versioning the Worker
Register once at app load and handle updates explicitly:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then((reg) => reg.addEventListener('updatefound', () => {
const sw = reg.installing;
sw.addEventListener('statechange', () => {
if (sw.state === 'installed' && navigator.serviceWorker.controller) {
// new version ready, prompt user to refresh
}
});
}));
}
Version your cache keys (transfer-v7) so a new deploy invalidates old assets without leaving stale JavaScript behind. The classic bug: index.html caches forever, users never get the update, and you're stuck debugging by Slack for weeks. Pin caches to build hashes and clean up in the activate event.
Caching Strategies for App Shell vs. User Data
Different resources deserve different strategies:
- App shell (HTML, CSS, JS, icons): cache-first with a network fallback. Instant loads, works offline.
- API metadata (
/shares/:id): network-first with a cache fallback, 60-second TTL. Fresh when online, usable when not. - File bytes: never cache. Files are often multi-gigabyte and the Cache API has origin quotas (typically 60% of free disk).
- Fonts from CDNs: stale-while-revalidate. Fast and auto-refreshing.
In the fetch handler:
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
if (url.pathname.startsWith('/assets/')) {
e.respondWith(cacheFirst(e.request, 'shell-v7'));
} else if (url.pathname.startsWith('/api/shares/')) {
e.respondWith(networkFirst(e.request, 'api-v1', 60));
}
});
Never intercept requests for binary file uploads — route around them by checking e.request.method === 'PUT' and returning early. Proxying gigabyte PUTs through the worker is a memory disaster.
Queuing Failed Uploads With Background Sync
SyncManager is the key to resilient uploads. When a chunk PUT fails, stash it in IndexedDB and register a sync:
// in page code
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('flush-uploads');
// in sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'flush-uploads') {
event.waitUntil(flushPendingUploads());
}
});
The browser fires the sync event once network comes back, with exponential backoff up to ~24 hours. Chrome and Edge support it; Safari shipped a subset in 17.5 under "Background Fetch" flag but requires user permission. For Safari, fall back to retrying on the next visible page open via visibilitychange.
Background Fetch API is a separate tool specifically for large file operations — it shows a persistent notification in the browser UI so users can track progress even after closing the tab. Worth using for uploads over 500 MB.
Storing Partial Uploads in IndexedDB
IndexedDB is your durable scratch space. Open a small database once, then stash session metadata plus chunk offsets:
const db = await openDB('transfers', 1, {
upgrade(db) {
db.createObjectStore('sessions', { keyPath: 'id' });
db.createObjectStore('chunks', { keyPath: ['sessionId', 'index'] });
}
});
await db.put('sessions', {
id, fileName, fileSize, fileFingerprint, createdAt: Date.now(),
completedIndexes: [], partUrls
});
Don't store raw chunk bytes — they come from the File handle, which IndexedDB can persist as a structured clone reference that stays valid across reloads. Storing the handle avoids duplicating 2 GB of bytes in the database.
Origin storage has limits: roughly 60% of free disk on desktop Chrome, 1 GB per origin on iOS Safari before eviction pressure kicks in. Request navigator.storage.persist() to get the "persistent" bucket that browsers avoid evicting automatically.
Handling Offline and Online Transitions
Listen for online and offline events, both in the page and the service worker:
// page
window.addEventListener('online', () => {
ui.showBanner('Back online — resuming uploads');
navigator.serviceWorker.controller?.postMessage({ type: 'resume' });
});
window.addEventListener('offline', () => {
ui.showBanner('Offline — uploads paused');
});
navigator.onLine is famously unreliable on corporate captive portals — it reports true when the device has a local network connection but no internet. For trustworthy detection, do a tiny fetch('/ping', { cache: 'no-store' }) with a 3-second timeout.
Making It a Proper PWA
Ship a manifest.json with display: standalone, an icon set, and start_url: /. Add apple-touch-icon links for iOS. Declare file handling so the OS can associate your app with specific extensions:
{
"name": "Hex Transfer",
"file_handlers": [{
"action": "/share-target",
"accept": { "application/*": [".pdf", ".zip", ".docx"] }
}]
}
Combined with Web Share Target, this lets users share files from their OS share sheet straight into your app. On Chrome Android and desktop Chromium, the PWA can register as a default handler for the file types you declare. This turns a browser page into an app that behaves like a native upload tool.
Testing the Offline Story
Three scenarios to test manually, because automated offline tests are flaky:
- Start a 500 MB upload on fast Wi-Fi, switch to airplane mode at 30%, wait 30 seconds, turn Wi-Fi back on. The upload should resume from where it stopped without user action.
- Start an upload, close the tab at 60%, wait 2 minutes, reopen. Offer to resume the session.
- Start an upload on mobile, lock the screen for 5 minutes. Background sync should fire when you unlock and finish the transfer.
Chrome DevTools' "Offline" checkbox and Application > Service Workers > Update on reload are indispensable. The Network panel's "Throttling" profiles let you simulate Fast 3G and Slow 3G to see how your error UI behaves.
HexaTransfer's web app uses a Service Worker for app-shell caching and IndexedDB for in-flight session state, so reloads and brief offline periods don't lose upload progress. Try it at hexatransfer.com — free, no account, 10 GB max.
Pitfalls Worth Knowing About
Service Workers have a small pile of gotchas that bite newcomers: they only work on HTTPS (except localhost), cache quotas vary wildly across browsers, iOS Safari doesn't reliably wake workers for Background Sync, DevTools can cache stale workers aggressively (always click "Bypass for network" while developing), and importScripts runs synchronously during install so never fetch slow third-party scripts there. Write a small integration test that checks the worker activates, claims clients, and serves the offline page — that single test catches 80% of the regressions you'll hit in production.
Send large files securely with end-to-end encryption
Transfer files up to 10 GB for free with end-to-end encryption. No account required. Your files are encrypted in your browser before upload — no one else can read them.
Send a file