---
title: Downloading Files in the Background in Capacitor
description: Learn how a Capacitor background download survives backgrounding, OS kills and force-quits on Android and iOS, and how to restore it after a relaunch.
date:
  created: 2026-10-05
  updated: 2026-10-05
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor File Transfer: sdks/capacitor/file-transfer.md
faq: true
---

# Downloading Files in the Background in Capacitor

A download in a Capacitor app keeps running after the user leaves the app only when native code performs it, and how far it gets depends on how the app was closed. A Capacitor background download continues on Android and iOS while the app is backgrounded, survives an operating system kill on iOS, and is canceled or interrupted when the user force-quits the app. This guide explains why downloads in JavaScript cannot do this, how the [Capacitor File Transfer plugin](../../sdks/capacitor/file-transfer.md) hands the work to the platform's transfer engines, and how your app picks up the result after a relaunch. The plugin is part of [Capawesome Insiders](../../insiders/index.md).

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Thousands of teams ship faster with Capawesome Cloud Native Builds and Live Updates" src="https://capawesome.io/assets/banners/cloud-teams-ship-faster-with-capacitor.png" />
  </a>
</div>

**Key takeaways:**

- iOS gives `applicationDidEnterBackground(_:)` five seconds and then suspends the app, so a `fetch` or XHR download in the web view stops with the app's process.
- The Capacitor File Transfer plugin downloads through a background `URLSession` on iOS, which runs in a separate system process, and a `dataSync` foreground service on Android.
- After a low-memory kill, iOS finishes the download and delivers it on relaunch, while Android interrupts it and restores it as `failed` and resumable.
- A force-quit cancels the transfer on iOS, and the plugin retries it on the next launch only when `maxRetries` is greater than 0.
- `transferCompleted` and `transferFailed` events that fire without a listener are retained, so listeners registered at startup receive results from the previous process.

## Why fetch stops

A download started with `fetch`, `XMLHttpRequest` or another JavaScript API runs in the web view inside your app's process, so it stops when iOS suspends that process. Apple's [background execution guide](https://developer.apple.com/documentation/uikit/extending-your-app-s-background-execution-time){:target="_blank"} spells out how quickly that happens:

> When your app moves to the background, the system calls your app delegate's applicationDidEnterBackground(_:) method. That method has five seconds to perform any tasks and return. Shortly after that method returns, the system puts your app into the suspended state.

`beginBackgroundTask(...)` buys extra time on top of that, but the time is finite, and Apple adds that "if you don't end your tasks in a timely manner, the system terminates your app." A large video rarely finishes downloading in that window on a mobile connection.

Background-mode plugins that keep the web view alive run into two problems. They work against Apple's rule that "a background app must do as little work as possible, and preferably nothing, because it's offscreen", and even when they succeed, the download still lives in the app's process. An OS kill or a force-quit ends it together with the app. The [background audio guide](./how-to-play-audio-in-the-background-in-capacitor.md) runs into the same suspension for HTML5 audio and solves it the same way, with native code.

The official Capacitor plugins do not change this picture on paper. The [`@capacitor/file-transfer`](https://capacitorjs.com/docs/apis/file-transfer){:target="_blank"} documentation (version 2.0.6 on npm) describes `downloadFile(...)` as a request that downloads "the file to the specified destination", and it does not document background, suspension or app lifecycle behavior anywhere on the page. The method returns a promise in your JavaScript, so a relaunched app has no reference to a download that the previous process started. [`@capacitor/http`](https://capacitorjs.com/docs/apis/http){:target="_blank"} patches `fetch` and `XMLHttpRequest` to use native libraries, does not document background execution either, and points large files to `@capacitor/file-transfer`.

## Native transfer engines

The Capacitor File Transfer plugin moves the download out of your app's process on iOS and into a component that Android allows to run in the background. Each platform gets the engine that the operating system designed for this job.

On iOS, the plugin uses a background `URLSession`. Apple's guide to [downloading files in the background](https://developer.apple.com/documentation/foundation/downloading-files-in-the-background){:target="_blank"} describes the property that makes this work:

> With background sessions, the actual transfer is performed by a process that is separate from your app's process.

Because the system process owns the transfer, iOS can suspend your app, or terminate it for memory, without touching the download. When the download finishes, iOS wakes or relaunches the app in the background to hand over the result. Apple documents one limit of this engine: for a transfer that an app starts while it is already in the background, the system [treats the session as discretionary](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/isdiscretionary){:target="_blank"} and decides when the transfer runs. Start important downloads while the app is in the foreground.

On Android, the plugin runs transfers in a `dataSync` foreground service driven by its own OkHttp engine. Android's [foreground service documentation](https://developer.android.com/develop/background-work/services/fgs){:target="_blank"} ties this kind of service to a visible status bar notification, "to make users aware that your app is performing a task in the foreground and is consuming system resources." That notification is the price of running while the app is backgrounded.

Android adds two rules on top. Apps that target Android 12 or higher [cannot start a foreground service](https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start){:target="_blank"} while they run in the background. A transfer that starts in that situation, for example after waiting for Wi-Fi, runs without the foreground service and its notification since plugin version 0.1.2, instead of failing. Second, Android 15 introduced a [timeout for `dataSync` services](https://developer.android.com/about/versions/15/behavior-changes-15#datasync-timeout){:target="_blank"}: apps that target API level 35 or higher may run them for a total of 6 hours in a 24-hour period, shared by all of the app's `dataSync` services, and the timer resets when the user brings the app to the foreground.

## Start a download

To install the Capacitor File Transfer plugin, please refer to the [Installation](../../sdks/capacitor/file-transfer.md/#installation) section in the plugin documentation. The plugin requires Capacitor 8 or later. Once it is installed, [`startDownload(...)`](../../sdks/capacitor/file-transfer.md#startdownload) starts a download and resolves immediately with its identifier, while the transfer continues in native code:

```typescript
import { FileTransfer } from '@capawesome-team/capacitor-file-transfer';

const downloadCoursePack = async (url: string, path: string) => {
  const { id } = await FileTransfer.startDownload({
    url,
    path,
    network: 'unmetered',
    maxRetries: 3,
    androidNotification: {
      title: 'Downloading course pack',
      text: 'The download continues in the background.',
      progress: true,
    },
  });
  return id;
};
```

`url` is the source and `path` is the device path where the plugin stores the file. `headers` adds request headers such as `Authorization`, and `method` accepts `GET` (the default) or `POST`. The remaining options shape the background behavior:

- **`network`**: `'unmetered'` restricts the transfer to networks such as Wi-Fi and waits until one is available. On Android, the transfer stays in the `pending` state while it waits. The default is `'any'`.
- **`maxRetries`**: how often the plugin retries the transfer after a network error, with backoff. The default is `0`, which matters again after a force-quit on iOS.
- **`androidNotification`**: the title and text of the foreground service notification on Android. `progress: true` adds a separate notification with a progress bar for this transfer, and `channelName` defaults to `File Transfer`.

Downloads are resumable by default, which requires the server to support the HTTP `Range` header; the [pause and resume section](../../sdks/capacitor/file-transfer.md#pause-and-resume-a-transfer) of the documentation covers the `resumable` option. On the web, `startDownload(...)` rejects as unavailable, because transfers need a native background API.

## Listen for results

The Capacitor File Transfer plugin reports a download through three events instead of the promise of `startDownload(...)`, because the promise resolves before any byte arrives. Register the listeners once, at app startup, rather than on the screen that started the download:

```typescript
import { FileTransfer } from '@capawesome-team/capacitor-file-transfer';

const addTransferListeners = async () => {
  await FileTransfer.addListener('transferProgress', event => {
    console.log(`Transfer ${event.id}: ${event.progress ?? 'unknown'}`);
  });
  await FileTransfer.addListener('transferCompleted', event => {
    console.log(`Transfer ${event.id} saved to ${event.path}`);
  });
  await FileTransfer.addListener('transferFailed', event => {
    console.error(`Transfer ${event.id} failed: ${event.errorCode}`, event.message);
  });
};
```

[`transferProgress`](../../sdks/capacitor/file-transfer.md#addlistenertransferprogress-) fires roughly every 100 milliseconds per transfer with `bytes`, `totalBytes` and a `progress` value between 0 and 1, which is `null` when the server sends no size. [`transferCompleted`](../../sdks/capacitor/file-transfer.md#addlistenertransfercompleted-) carries the file `path` and the HTTP `responseCode`, and `transferFailed` carries an `errorCode`, a `message` and the `responseCode`.

The two final events behave differently from progress. The plugin retains completed and failed events that occur while no listener is registered and delivers them once one is added. The documentation makes no such promise for progress events while the app is suspended, so base your logic on the final events and treat progress as display only.

## Platform setup

The Capacitor File Transfer plugin needs one change in your iOS project and none in your Android manifest. On iOS, the system relaunches a terminated app in the background when its URL session finishes, and Apple's [`application(_:handleEventsForBackgroundURLSession:completionHandler:)`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application%28_:handleeventsforbackgroundurlsession:completionhandler:%29){:target="_blank"} documentation describes the hand-off: "If a URL session finishes its work when your app is not running, the system launches your app in the background so that it can process the event." The plugin needs that completion handler, so you forward it from an `AppDelegate` extension:

```swift
import Foundation

extension AppDelegate {
    func application(
        _ application: UIApplication,
        handleEventsForBackgroundURLSession identifier: String,
        completionHandler: @escaping () -> Void
    ) {
        NotificationCenter.default.post(
            name: Notification.Name("io.capawesome.capacitorjs.plugins.filetransfer.handleEventsForBackgroundURLSession"),
            object: completionHandler
        )
    }
}
```

Without this hook, background transfers still complete, but iOS may not be able to wake the app to deliver the final events promptly. The [iOS section](../../sdks/capacitor/file-transfer.md#ios) of the documentation lists no `Info.plist` entry or background mode for the plugin.

On Android, the plugin declares the `INTERNET`, `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_DATA_SYNC` and `POST_NOTIFICATIONS` permissions and the `dataSync` service in its own manifest. The one runtime step is the notification permission on Android 13 and higher, which [`requestPermissions()`](../../sdks/capacitor/file-transfer.md#requestpermissions) requests:

```typescript
import { FileTransfer } from '@capawesome-team/capacitor-file-transfer';

const requestNotificationPermission = async () => {
  const { notifications } = await FileTransfer.requestPermissions();
  return notifications === 'granted';
};
```

A denied permission hides the progress notification, and the transfer still runs. Android's [notification permission guide](https://developer.android.com/develop/ui/views/notifications/notification-permission){:target="_blank"} adds that users then still see the foreground service in the Task Manager, only not in the notification drawer. On Android 12 and older and on iOS, the method resolves with `granted` without prompting.

## What survives what

A Capacitor background download with the Capacitor File Transfer plugin survives backgrounding on both platforms, survives an OS kill only on iOS, and survives a force-quit on neither. The plugin's [FAQ on background behavior](../../sdks/capacitor/file-transfer.md#do-transfers-continue-when-the-app-is-in-the-background) documents each state, summarized here:

| App state | Android | iOS |
| --- | --- | --- |
| Backgrounded | Keeps running in the `dataSync` foreground service. | Keeps running in the background `URLSession`. |
| Killed by the OS for memory | Interrupted, then restored as `failed`; downloads can be resumed. | Finished by the system and delivered when the app relaunches. |
| Force-quit by the user | Same as an OS kill. | Canceled by the system; retried on relaunch if `maxRetries` is above 0, otherwise restored as `failed` with a `transferFailed` event. |

The iOS force-quit row is an operating system rule. Apple's [`background(withIdentifier:)`](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/background%28withidentifier:%29){:target="_blank"} documentation states it in full:

> This behavior applies only for normal termination of the app by the system. If the user terminates the app from the multitasking screen, the system cancels all of the session's background transfers. In addition, the system does not automatically relaunch apps that were force quit by the user. The user must explicitly relaunch the app before transfers can begin again.

The plugin cannot restart a canceled download before the user opens the app again, so the retry happens on relaunch, and only when `maxRetries` is above its default of 0. Since version 0.1.2, the plugin also marks a transfer that iOS canceled as `failed` instead of leaving it `running`.

On Android, an interrupted download does not continue by itself. It waits in the `failed` state until your code calls [`resumeTransferById(...)`](../../sdks/capacitor/file-transfer.md#resumetransferbyid), which continues from the bytes already on disk when the server supports the HTTP `Range` header.

## Restore after relaunch

After a relaunch, [`getTransfers()`](../../sdks/capacitor/file-transfer.md#gettransfers) returns every transfer the Capacitor File Transfer plugin knows about, including transfers restored from the previous process. Call it on startup, after registering the listeners, to rebuild your download list:

```typescript
import { FileTransfer } from '@capawesome-team/capacitor-file-transfer';

const restoreDownloads = async () => {
  const { transfers } = await FileTransfer.getTransfers();
  return transfers.filter(
    transfer => transfer.type === 'download' && transfer.state !== 'canceled',
  );
};
```

Each `Transfer` carries its `id`, `state`, `url`, `path`, `bytes` and `totalBytes`, which is enough to render a row per download without your own persistence layer. A `running` download keeps reporting through `transferProgress`, a `pending` one waits for its network, and a `failed` one is the candidate for a resume button.

The listener order matters for downloads that finished while the app was not running. A download that iOS completed after an OS kill produces a `transferCompleted` event in the relaunched app, and the plugin holds it until the first listener registers. If you register listeners only when the user opens the downloads screen, the event arrives there, possibly long after the file exists. The full set of transfer states is listed in the [`Transfer`](../../sdks/capacitor/file-transfer.md#transfer) reference.

## FAQ

### Do Capacitor downloads keep running when the app is closed?

It depends on how the app was closed. With the [Capacitor File Transfer plugin](../../sdks/capacitor/file-transfer.md), a download keeps running while the app is backgrounded on Android and iOS. If the OS kills the app for memory, iOS finishes the download and delivers it on relaunch, while Android interrupts it and restores it as `failed` and resumable. If the user force-quits the app, iOS cancels the download and the plugin retries it on relaunch only with `maxRetries` above 0; Android handles it like an OS kill. A download in `fetch` or XHR stops when iOS suspends the app.

### Does a background download on Android need a notification?

Yes, while it runs in the foreground service, because Android requires a notification for every foreground service. The `POST_NOTIFICATIONS` permission only controls whether the notification is visible on Android 13 and higher, and the download runs either way. A download that starts while the app is in the background on Android 12 or higher runs without the service and its notification.

### Does @capacitor/file-transfer download in the background?

The official `@capacitor/file-transfer` documentation does not document background or app lifecycle behavior. Its `downloadFile(...)` method returns a promise in JavaScript, so there is no documented way to pick up a download after the app was killed. [An Alternative to cordova-plugin-file-transfer](./alternative-to-cordova-plugin-file-transfer.md) compares it with the Capacitor File Transfer plugin.

### Do uploads continue in the background too?

Yes. [`startUpload(...)`](../../sdks/capacitor/file-transfer.md#startupload) uses the same engines and the same events as `startDownload(...)`. Only downloads can be paused and resumed in the current version.

### Is the Capacitor File Transfer plugin free?

No. The plugin is part of the [Capawesome Insiders](../../insiders/index.md) subscription and is installed from the Capawesome npm registry with the license key that comes with it.

## Related posts

- [Announcing the Capacitor File Transfer Plugin](./announcing-the-capacitor-file-transfer-plugin.md)
- [An Alternative to cordova-plugin-file-transfer](./alternative-to-cordova-plugin-file-transfer.md)
- [Capacitor File Handling: The Complete Guide](./capacitor-file-handling-guide.md)
- [How to Play Audio in the Background in a Capacitor App](./how-to-play-audio-in-the-background-in-capacitor.md)

## Try Capawesome Cloud

Background downloads need native code and an `AppDelegate` change on iOS, so shipping them takes a new native build rather than a web update. Capawesome Cloud builds and signs that app for Android and iOS without a Mac, and [How to Sign & Build Capacitor Apps in the Cloud](./how-to-sign-and-build-your-capacitor-app-in-the-cloud.md) shows the steps.

[Try Capawesome Cloud Free](https://capawesome.io){ .md-button .md-button--primary }

## Conclusion

Use a native transfer for any download that takes longer than a few seconds or that the user should be able to leave, and keep `fetch` for small requests that finish while the screen is open. Set `maxRetries` above 0 for downloads the user started, so an iOS force-quit costs a relaunch rather than the download, and call `getTransfers()` on every launch so an Android download interrupted in the background gets its resume button. For the files the downloads produce, [Capacitor File Handling: The Complete Guide](./capacitor-file-handling-guide.md) covers reading, moving and opening them.

If you have any questions, join us on the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}. To stay updated on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
