
How I Finally Rescued My App's Largest Transfers: A Journey Into the Background Fetch API
I stopped losing multi-gigabyte uploads to accidental tab swipes after offloading the transfer lifecycle to the browser's persistent background layer.
There is a specific kind of developer heartbreak that only occurs when a 4GB upload hits 98% and the user accidentally swipes the tab closed. You can almost hear the silent scream of lost packets echoing through the fiber optic cables.
For a long time, we just accepted this as the "nature of the web." If the document dies, the process dies. But I finally got tired of telling users to "keep the tab open and don't let your computer sleep," so I dove into the Background Fetch API. It’s the closest thing we have to a "download manager" built directly into the browser.
The Problem with Standard Fetch
When you trigger a regular fetch() or even an XMLHttpRequest, that request is tethered to the lifecycle of your tab. If the user navigates away, closes the browser, or if the OS decides your mobile browser is hogging too much RAM and kills it, that transfer is toast.
The Background Fetch API flips the script. Instead of the page managing the transfer, the page hands a "request object" to the browser. The browser then handles the heavy lifting in a persistent process that survives tab closures and even browser restarts.
Step 1: Registering the Hand-off
You can't use Background Fetch without a Service Worker. The Service Worker acts as the persistent brain that stays awake even when your UI is gone.
First, we check for support (because, let's be real, Safari support is still a moving target) and then trigger the fetch.
async function startLargeUpload(files) {
const registration = await navigator.serviceWorker.ready;
// Check if the API is actually there
if (!('backgroundFetch' in registration)) {
console.error("Your browser is living in 2015. No Background Fetch for you.");
return;
}
try {
const bgFetch = await registration.backgroundFetch.fetch('video-upload-123',
['/api/upload-part1', '/api/upload-part2'], {
icons: [{
src: '/upload-icon.png',
sizes: '192x192',
type: 'image/png',
}],
title: 'Uploading your cinematic masterpiece...',
downloadTotal: 2 * 1024 * 1024 * 1024 // 2GB total
});
// You can listen to progress while the tab is still open
bgFetch.addEventListener('progress', () => {
if (!bgFetch.downloadTotal) return;
const percent = Math.round(bgFetch.downloaded / bgFetch.downloadTotal * 100);
console.log(`Upload progress: ${percent}%`);
});
} catch (err) {
console.error("Transfer failed to even start:", err);
}
}The id ('video-upload-123') is crucial. It’s how you’ll reconnect to this specific transfer later if the user reopens your app.
Step 2: The Service Worker Logic
This is where the real rescue happens. The browser will handle the bits and bytes, but it needs to know what to do when it finishes (or if it hits a brick wall). You handle this in your sw.js file.
// sw.js
self.addEventListener('backgroundfetchsuccess', (event) => {
const registration = event.registration;
event.waitUntil(async function() {
// At this point, the browser has finished the transfer.
// We now need to "pick up" the results.
const records = await registration.matchAll();
// We can loop through the records and see what the server sent back
const promises = records.map(async (record) => {
const response = await record.responseReady;
return response.json();
});
const results = await Promise.all(promises);
// Tell the user it's done via a notification
self.registration.showNotification('Upload Complete!', {
body: 'Your 2GB 4K video of your cat is now live.',
icon: '/check-mark.png'
});
// Finalize the fetch (mandatory!)
await event.updateUI({ title: 'Upload successful!' });
}());
});
self.addEventListener('backgroundfetchfail', (event) => {
console.error("Transfer went south. Cleaning up...");
// You might want to log this to your telemetry
});Why This is a UX Game Changer
The magic here isn't just "it doesn't break." It's the User Interface. When you trigger a Background Fetch, the browser (Chrome, Edge, etc.) actually shows a native progress bar in the operating system's notification area or download tray.
The user can literally close Chrome, go get a coffee, and see the progress in their Windows or Android notification shade. That is a level of "pro app" feel that we couldn't achieve with standard web tech.
The "Gotchas" (The Bitter Pill)
It sounds perfect, but I've spent enough hours debugging this to know the traps:
1. Quota: Browsers still impose storage limits. If you're trying to fetch 50GB and the user's disk is nearly full, the browser will reject it.
2. Server Support: Your backend needs to be ready for this. Since Background Fetch often involves multiple requests or resumable uploads, you need to ensure your server doesn't choke on the way the browser sends the data.
3. The "Fetch" in "Background Fetch": It's primarily designed for *getting* resources, but it works for *sending* them too (using POST requests in the request array). However, the API is much more ergonomic for downloading.
4. Privacy: The browser will show the user exactly what's happening. You can't "hide" a background fetch. This is a feature, not a bug, but it's something to keep in mind for app design.
How to Handle Re-attachment
If the user closes the tab and reopens it five minutes later, you want your UI to reflect the ongoing progress. You can query existing fetches when your app boots up:
navigator.serviceWorker.ready.then(async (reg) => {
const ids = await reg.backgroundFetch.getIds();
if (ids.includes('video-upload-123')) {
const existingFetch = await reg.backgroundFetch.get('video-upload-123');
// Re-bind your UI listeners here
updateMyProgressBar(existingFetch);
}
});Moving Forward
Implementing Background Fetch turned my app from "clunky web tool" into "reliable workstation software." It’s about respecting the user's time and their bandwidth. We shouldn't punish someone for wanting to close a tab or for their Wi-Fi flaking out for thirty seconds.
If you’re dealing with anything larger than a few megabytes—videos, large PDF archives, or game assets—stop relying on the document lifecycle. Let the browser take the wheel. It's much better at driving in the background than we are.