# Background Fetch API

> How BackgroundFetchManager.fetch() hands a large download to the browser, which shows progress and a cancel control and wakes the service worker with backgroundfetchsuccess, backgroundfetchfail, backgroundfetchabort, or backgroundfetchclick, with the exceptions and failure reasons.

The Background Fetch API lets a page or service worker hand a set of requests to the browser as one named download that continues after the tab closes; the browser shows progress and a cancel control in its own UI and fires an event in the service worker when the operation settles. It shipped in Chrome 74, Edge 79, and Samsung Internet 11 and is not implemented in Firefox or Safari (BCD `api.BackgroundFetchManager`), so it is an enhancement over a normal `fetch()` download, not a replacement for one.

## Syntax

```js
// Page or service worker; registration comes from navigator.serviceWorker.ready
const bgFetch = await registration.backgroundFetch.fetch(id, requests, options);
const existing = await registration.backgroundFetch.get(id);     // undefined when no job has that id
const ids = await registration.backgroundFetch.getIds();         // string[]

// Service worker
self.addEventListener('backgroundfetchsuccess', (event) => { /* event.registration */ });
self.addEventListener('backgroundfetchfail', (event) => { /* event.registration.failureReason */ });
self.addEventListener('backgroundfetchabort', (event) => { /* user or app cancelled */ });
self.addEventListener('backgroundfetchclick', (event) => { /* user tapped the browser UI */ });
```

`fetch()` returns a `Promise<BackgroundFetchRegistration>` as soon as the browser has accepted the job, long before any bytes arrive. Exactly one of `backgroundfetchsuccess`, `backgroundfetchfail`, or `backgroundfetchabort` fires when the operation settles; `backgroundfetchclick` fires only if the user activates the browser's download UI. The first two are `BackgroundFetchUpdateUIEvent` instances whose `updateUI({ title, icons })` changes the finished notification; the other two are plain `BackgroundFetchEvent` instances.

## Parameters

`fetch()` takes two required arguments and one options object.

| Name | Type | Description |
|---|---|---|
| `id` | `string` | Developer-chosen identifier, later passed to `get(id)`. One active registration per id; reusing an id while the first is in flight rejects. |
| `requests` | `RequestInfo \| RequestInfo[]` | One `Request` or URL string, or an array of them. All requests are fetched as one user-visible job. Requests with `mode: 'no-cors'` are rejected. |
| `options.title` | `string` | Title shown in the browser's progress UI. |
| `options.icons` | `ImageResource[]` | Objects with `src`, optional `sizes`, `type`, and `label`, used in the progress UI. |
| `options.downloadTotal` | `number` | Estimated total size in bytes. It drives the progress bar and is a hard cap: the browser aborts the job once downloaded bytes exceed it, with `failureReason` set to `"download-total-exceeded"`. |

The returned `BackgroundFetchRegistration` exposes `id`, `downloadTotal`, `downloaded`, `uploadTotal`, `uploaded`, `result` (`""`, `"success"`, or `"failure"`), `failureReason` (`""`, `"aborted"`, `"bad-status"`, `"fetch-error"`, `"quota-exceeded"`, or `"download-total-exceeded"`), `recordsAvailable`, `abort()`, `match()`, `matchAll()`, and a `progress` event that fires whenever any of the counters or `result` changes.

## Exceptions

`fetch()` rejects its promise in these cases (MDN, `BackgroundFetchManager.fetch()`).

- `TypeError`: no request was given, a request uses `mode: 'no-cors'`, the registration has no service worker, a registration with the same `id` already exists, or a request could not be created.
- `AbortError` (`DOMException`): the fetch was aborted before it was accepted.
- `NotAllowedError` (`DOMException`): the user agent has not granted permission to make background fetches for this origin; the specification leaves that permission check to the browser, so the condition is browser-defined.
- `QuotaExceededError`: storing the requests exceeded the origin's storage quota. Responses are held in Cache Storage-like storage until the service worker reads them, so they count against the same quota as `caches`.

A failed download does not reject anything: it fires `backgroundfetchfail` with `event.registration.failureReason` set. A `"bad-status"` reason means a response had a non-ok status; `"fetch-error"` means the network request itself failed after retries.

:::observed
In Chrome (English UI), DevTools › Application › Background services › Background fetch stays empty until the record button (tooltip "Start recording events") is pressed; after that every state change of a registration appears as a row in a table whose columns are Timestamp, Event, Origin, Service Worker Scope, and Instance ID, and selecting a row shows the registration's `id` in the lower pane. The recording keeps running for up to three days while DevTools is closed, which is how a download that finished after the tab was closed can be inspected afterwards (developer.chrome.com, "Debug background services").
:::

## Examples

Both examples assume a service worker registered at `/sw.js` and a secure origin.

### Downloading an episode and its artwork as one job

The page requests the audio file and its cover image under one id so the browser shows a single progress notification; `downloadTotal` is the sum of the two `Content-Length` values obtained earlier from the server's API.

```js
async function downloadEpisode(episode) {
  const registration = await navigator.serviceWorker.ready;
  const bgFetch = await registration.backgroundFetch.fetch(
    `episode-${episode.id}`,
    [episode.audioUrl, episode.artworkUrl],
    {
      title: episode.title,
      icons: [{ src: '/icons/download-192.png', sizes: '192x192', type: 'image/png', label: 'Episode download' }],
      downloadTotal: episode.audioBytes + episode.artworkBytes,
    },
  );
  bgFetch.addEventListener('progress', () => {
    const percent = Math.round((bgFetch.downloaded / bgFetch.downloadTotal) * 100);
    document.querySelector('#progress').value = percent;
  });
}
```

The `progress` listener only runs while this page is open; once the tab closes, the browser's own UI is the only progress indicator, which is the point of the API.

### Storing the responses when the job settles

The service worker copies each response into a named cache in `backgroundfetchsuccess`, then changes the notification text; on failure it logs the reason and leaves the notification untouched so the user sees the browser's failure state.

```js
self.addEventListener('backgroundfetchsuccess', (event) => {
  event.waitUntil((async () => {
    const cache = await caches.open('episodes');
    const records = await event.registration.matchAll();
    await Promise.all(records.map(async (record) => {
      const response = await record.responseReady;
      await cache.put(record.request, response);
    }));
    await event.updateUI({ title: `${event.registration.id} is ready to play` });
  })());
});

self.addEventListener('backgroundfetchfail', (event) => {
  console.warn('background fetch failed:', event.registration.failureReason);
});

self.addEventListener('backgroundfetchclick', () => {
  clients.openWindow('/downloads/');
});
```

`record.responseReady` is a promise because a record's response may still be streaming when the event fires for a partially failed job; awaiting it inside `waitUntil()` keeps the worker alive until the copy finishes.

### Detecting support and downloading in the foreground instead

Firefox and Safari have no `backgroundFetch` property on the registration. The fallback performs an ordinary `fetch()` and triggers the browser's save dialog through a temporary `<a download>`, which only works while the page stays open; the UI should say so.

```js
async function downloadWithFallback(id, urls, title) {
  const registration = await navigator.serviceWorker.ready;
  if ('backgroundFetch' in registration) {
    return registration.backgroundFetch.fetch(id, urls, { title });
  }
  for (const url of urls) {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`${url}: ${response.status}`);
    const link = document.createElement('a');
    link.href = URL.createObjectURL(await response.blob());
    link.download = new URL(url, location.href).pathname.split('/').pop();
    link.click();
    URL.revokeObjectURL(link.href);
  }
}
```

The foreground path buffers each file in memory before saving it, so very large files should be fetched one at a time and the user warned not to navigate away.

## See also

- [Background Fetch specification: fetch() method](https://wicg.github.io/background-fetch/#background-fetch-manager-fetch) (wicg.github.io)
- [Introducing Background Fetch](https://developer.chrome.com/blog/background-fetch) (developer.chrome.com)
- [Debug background services](https://developer.chrome.com/docs/devtools/javascript/background-services) (developer.chrome.com)
- [Background Fetch browser support](/compatibility/background-fetch/)
- [Background Sync API](/reference/service-worker/background-sync/)
- [Cache API](/reference/service-worker/cache-api/)
- [Quotas and StorageManager.estimate()](/reference/storage/quota-estimate/)