---
title: 'Background Transfers'
source: 'https://academia.sh/en/courses/data-networking-and-concurrency/background-transfers'
course: 'Data, Networking and Concurrency'
language: en
updated: '2026-10-21T05:03:09+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains that a background session continues the transfer in a process separate from the app'
    - 'Measures that queuing without checking the transfers the system kept running on relaunch duplicates the file'
    - 'Shows that keeping the registry by task identifier stays robust in the same case'
    - 'States that when the system will continue the transfer remains outside the model'
---

# Background Transfers

Downloads the system keeps running while the app is in the background; measuring why re-queuing on relaunch without checking the list downloads the same file twice.

A user can start downloading a large file and leave the app. In an ordinary session the transfer is cut off when the app moves to the background. A **background transfer** changes this: the system runs the transfer, and it continues even if the app is terminated by the system. When it finishes, or when needed, it wakes the app.

This arrangement has a consequence: when the app is relaunched, some transfers are already running. The app's own list does not know this. Re-queuing the list as it is can mean starting a second time a transfer the system is already running. The core is the same.

```swift
// Replay.swift — classifies an operation's sensitivity to repetition and order
struct Trial<State: Equatable & CustomStringConvertible> {
    let label: String
    let once: State
    let twice: State
    let forward: State
    let backward: State

    var sensitivity: String {
        switch (once != twice, forward != backward) {
        case (false, false): return "robust"
        case (true, false): return "repeat-sensitive"
        case (false, true): return "order-sensitive"
        case (true, true): return "doubly-sensitive"
        }
    }

    func report() -> String {
        print("\(label) | once=\(once) twice=\(twice) ab=\(forward) ba=\(backward) -> \(sensitivity)")
        return sensitivity
    }
}

func replay<State>(_ label: String, from start: State, _ a: (State) -> State, then b: (State) -> State) -> Trial<State> {
    Trial(label: label, once: a(start), twice: a(a(start)), forward: b(a(start)), backward: a(b(start)))
}

func countSensitivities(_ results: [String]) {
    for name in ["robust", "repeat-sensitive", "order-sensitive", "doubly-sensitive"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}
```

## The Transfer Registry

The model builds the transfer registry that the app knows. Each transfer is referred to by an identifier, and the registry is kept in sorted order. Two ways of queuing are compared: one that adds without looking at the identifier and one that looks first.

```swift
// Transfers.swift — the transfer registry and two ways of queuing
struct Registry: Equatable, CustomStringConvertible {
    var tasks: [String] = []
    var description: String { "[" + tasks.joined(separator: ",") + "]" }
}

func enqueueAll(_ files: [String]) -> (Registry) -> Registry {
    { registry in Registry(tasks: (registry.tasks + files).sorted()) }
}

func enqueueIfMissing(_ files: [String]) -> (Registry) -> Registry {
    { registry in
        let missing = files.filter { !registry.tasks.contains($0) }
        return Registry(tasks: (registry.tasks + missing).sorted())
    }
}

func finish(_ file: String) -> (Registry) -> Registry {
    { registry in Registry(tasks: registry.tasks.filter { $0 != file }) }
}
```

`enqueueAll` adds a new transfer for every file in the list. `enqueueIfMissing` adds only those that are not in the registry. `finish` builds the completion of a transfer. **The model builds only the app's registry; it is not the system's transfer execution or the disk itself.** The starting state in the measurement is a relaunch in which the system kept the download of `a.zip` running while the app was closed.

```swift
// main.swift — two ways of queuing on relaunch
let reopened = Registry(tasks: ["a.zip"])
let wanted = ["a.zip"]

let blind = replay("queue-without-checking", from: reopened, enqueueAll(wanted), then: enqueueAll(wanted)).report()
let byIdentifier = replay("by-identifier", from: reopened, enqueueIfMissing(wanted), then: enqueueIfMissing(wanted)).report()

countSensitivities([blind, byIdentifier])
```

```
queue-without-checking | once=[a.zip,a.zip] twice=[a.zip,a.zip,a.zip] ab=[a.zip,a.zip,a.zip] ba=[a.zip,a.zip,a.zip] -> repeat-sensitive
by-identifier | once=[a.zip] twice=[a.zip] ab=[a.zip] ba=[a.zip] -> robust
robust: 1
repeat-sensitive: 1
order-sensitive: 0
doubly-sensitive: 0
```

`queue-without-checking` is repeat-sensitive. The system was already running `a.zip`; when the list was re-queued a second `a.zip` record appeared, and when it was queued once more, a third. The same file is downloaded twice in a row. `by-identifier` is robust: because the identifier is in the registry, queuing adds nothing, however many times it is done.

**NW16.** The model counts the transfer the system kept running as present in the app's registry. In reality the app builds the registry by querying the system itself.

**NW17.** The model counts two records of the same file as two separate downloads. In a real app, if the two records write to the same target file, one may overwrite the other.

## The Background Session

A background transfer is set up with a special configuration that has an identifier.

```swift
import Foundation

let configuration = URLSessionConfiguration.background(withIdentifier: "downloads")
configuration.sessionSendsLaunchEvents = true
let session = URLSession(configuration: configuration, delegate: downloader, delegateQueue: nil)

let task = session.downloadTask(with: URL(string: "https://files.example.com/a.zip")!)
task.taskDescription = "a.zip"
task.resume()
```

This snippet stays SOURCE. The configuration's identifier makes it possible to reconnect to the same session when the app is relaunched. The reconnected session can query the tasks that are in progress.

```swift
import Foundation

func pendingFiles(of session: URLSession) async -> Set<String> {
    let tasks = await session.allTasks
    return Set(tasks.compactMap(\.taskDescription))
}
```

This also stays SOURCE. The model represents the result of this query as a `Registry`; `enqueueIfMissing` compares against this set before queuing. When the transfer finishes, a **delegate** receives the result, and the file must be moved from the temporary location to a permanent one.

```swift
import UIKit

final class AppDelegate: UIResponder, UIApplicationDelegate {
    var completionHandlers: [String: () -> Void] = [:]

    func application(_ application: UIApplication,
                     handleEventsForBackgroundURLSession identifier: String,
                     completionHandler: @escaping () -> Void) {
        completionHandlers[identifier] = completionHandler
    }
}
```

If the system wakes the app in the background, it calls this **app delegate** handler. The app tells the system it has finished its work by calling the stored **closure** when the events are done.

## The System Runs the Transfer

The basic idea of a background session is that the transfer does not depend on the lifetime of the app. In an ordinary session a download is cut off shortly after the app moves to the background, because the system suspends the app. In a background session, a system process separate from the app runs the download. Even if the app is suspended, or even closed by the system without being terminated by the user, the transfer can continue. When the job is done, the system wakes the app if needed and delivers the result.

The cost of this arrangement is that the app is no longer the only owner of the transfer. The system keeps the state of the transfer; the list in the app's hands is only a copy. While the app is closed, the transfer may have completed, failed, or still be running, and when the app is relaunched it cannot know which of these three it is without asking the system. The `Registry` in the model builds this copy of the app; the `queue-without-checking` line shows what happens in a relaunch where the copy does not match reality.

## Where It Shows Up in a Real App

In a podcast app, the user queues ten episodes for download and puts the phone in their pocket. The app moves to the background and the system continues the downloads. When the user opens the app an hour later, some of the episodes have been downloaded and some are still in progress. If the first thing the app does at launch is to re-queue the "to download" list, the episodes in progress are started a second time. The same file is downloaded twice; the data plan is spent, the battery drains, and if two transfers try to write to the same target file, the file can be corrupted.

A second case is the user closing the app themselves. If the user terminates the app from the multitasking screen, the system cancels the session's background transfers and does not launch the app on its own until the user opens it again. Because this behavior takes place in the system, it is not measured here; asking which transfers are still running when the app is relaunched is the only safe way to tell these two cases apart.

## The Wrong Setup and Its Fix

The wrong setup is to re-queue the saved list directly at launch.

```swift
import Foundation

// Wrong: at launch the list is re-queued as it is
func resumeDownloads(_ files: [URL], using session: URLSession) {
    for file in files {
        let task = session.downloadTask(with: file)
        task.taskDescription = file.lastPathComponent
        task.resume()
    }
}
```

This snippet stays SOURCE. The fix is to first ask the system for the tasks in progress and queue only the missing ones.

```swift
import Foundation

// Fix: ask for the running ones, queue the missing ones
func resumeDownloads(_ files: [URL], using session: URLSession) async {
    let running = Set(await session.allTasks.compactMap(\.taskDescription))
    for file in files where !running.contains(file.lastPathComponent) {
        let task = session.downloadTask(with: file)
        task.taskDescription = file.lastPathComponent
        task.resume()
    }
}
```

This also stays SOURCE. `taskDescription` is used here as the task identifier: it is written when queuing and read at launch. The `by-identifier` line in the measurement is the model of this: if the identifier is in the registry, queuing adds nothing, and however many times the operation is repeated, the registry stays the same. The robustness of the corrected function is against repetition; it is not against order. If a transfer is queued at the same moment it finishes, the result, as we saw in the measurement, depends on the order of the operations.

## The Session Lifecycle

A background session is referred to by its identifier, and this identifier is the session's identity. When the app sets up a new session with the same identifier, the system attaches the tasks in progress that belong to that identifier to the new session. If the identifier is generated differently at every launch, this link cannot be made; the old tasks are left without an owner and the app starts with a new set of tasks. The `by-identifier` line in the measurement therefore depends on a condition: the registry matching the system's real state and the identifier being persistent.

The lifecycle works like this. The app queues the task and moves to the background. The system runs the task. If the task finishes, or an event such as authentication is needed, the system launches the app in the background and delivers the events to the session's delegate. After the delegate processes the events, it calls the closure the system gave. After that the system may suspend the app again. When the user opens the app themselves, a session is set up with the same identifier and the pending events are delivered.

## The Check on Relaunch

The first job of a relaunch is to ask what the system has been doing. The list of tasks in progress is obtained, results that have completed but have not yet been processed are delivered to the delegate, and the registry is updated with this information. Only after that are new tasks queued for the files that are still missing. This order is the practical counterpart of the difference between `queue-without-checking` and `by-identifier`: the first considers the list correct by itself, the second asks for the system's truth.

Checking once is not enough. While the app stays open, tasks also complete, fail, or are cancelled. If the registry is not kept current with these events, the next launch checks with old information. This is the reason `finish` was put into the model: a finished transfer needs to leave the registry, otherwise the registry assumes a transfer that does not exist. The order sensitivity we see in the bounding measurement arises from the timing of this update: the finish notification can come before or after queuing, and it changes the result.

## Errors and Cancellation

A background transfer can fail as well: the server may not respond, the connection may drop, or the file may be cut off halfway. A failed task is reported to the delegate with an error. In some errors the system gives, inside the error, the data needed to continue from where it was cut off. If the app stores this data and starts the task from where it left off, the same bytes are not downloaded again. An app that does not do this downloads a large file left half done from the beginning.

When the user cancels a transfer, it also has to be removed from the registry. If the cancelled task stays in the registry, at the next launch the check thinks it is running and does not queue it again; the user asks when the file they cancelled will start downloading, or sees that the file never downloaded. This is another face of the principle that the record must reflect the state of the system.

## Verifying the Transferred File

A download finishing does not mean the file is correct. The server may have returned an error page with a successful response, or the file may have been left incomplete. Before moving it, the response's status code is checked, the file size is compared with the expected one, and if possible a digest value is verified. If an unverified file is taken to the permanent location, corrupt data stays for a long time without being noticed. When the user opens a video and finds that it cannot be played, they cannot understand why the app downloaded the file corrupt.

## Showing Progress

While a background transfer is running, the user may open the app and want to see the state of the transfer. The delegate reports the number of downloaded bytes at regular intervals; the app connects this information to a progress bar. Here too it should be kept in mind that the registry is not the single source of truth: when the app is relaunched, the progress information should be asked from the system again, because the bytes reported while the app was closed never reached the registry.

Progress information is not always exact. If the server does not report the total size, a percentage cannot be calculated and the bar turns into an indeterminate indicator. In this case showing a made-up percentage misleads the user; saying "downloading" is more honest. Sending a notification when the transfer completes is also common, but permission for notifications belongs to the user, and the app should keep working in a situation where it cannot show notifications.

## Constraints Tied to Network Conditions

The configuration of a background session also determines under what conditions the transfer will run. It is possible to choose downloading only on a wireless network, downloading on a connection that is not expensive, or downloading while charging. The tighter the constraint, the later the transfer starts. An app that promises a user a large file should state these conditions clearly; otherwise the user cannot understand why the transfer has not started.

The result of the constraint also shows in the measurement: because it is not known when the transfer will finish, the registry can stay in the "running" state for a long time. This is not a faulty record, it is an expected situation. Reading the record as an error leads the user to queue the same file again, and to a duplicate transfer.

## Common Mistakes

- Queuing the list on relaunch without asking. The transfer the system kept running is duplicated.
- Not moving the downloaded file before the delegate method returns. The temporary location is deleted.
- Generating the identifier differently at every launch. The app cannot reconnect to the old transfers.
- Relying on values held in a closure in a background session. The app may have been terminated.
- Predicting when the transfer will finish. That decision belongs to the system.

## Interpreting the Measured Result

The same two approaches were compared in the same relaunch, and a single check created the difference: looking at the identifier. Note that the queuing operation did not raise an error anywhere; three transfer records are a legal state. The duplication problem, as we often see in the networking lessons, is silent. Keeping the registry by identifier appeared as a one-line rule that solves the duplication; but for this rule to work, the record must be kept consistent with the system's real state.

## The Bounding Measurement

The model nowhere calculates when the system will finish the transfer or when it will wake the app. In the measurement below, the finish and queue operations are applied in two orders; the order is given to the model from outside.

```swift
// main.swift — finish and queue, two orders
let running = Registry(tasks: ["a.zip"])
let race = replay("finish-and-queue", from: running, finish("a.zip"), then: enqueueIfMissing(["a.zip"])).report()
countSensitivities([race])
```

```
finish-and-queue | once=[] twice=[] ab=[a.zip] ba=[] -> order-sensitive
robust: 0
repeat-sensitive: 0
order-sensitive: 1
doubly-sensitive: 0
```

It is order-sensitive: if the transfer finishes first and is queued afterward, the registry stays full; if it is queued first and finishes afterward, the registry empties. Which order will happen is not measured; in the model the order is an input, not a calculation. **The rule sees the order of the operations but does not see who determines the order.**

**NW18.** When the system will continue the transfer, when it will wake the app, and under what conditions it will defer were not measured. This behavior depends on system conditions.

## Summary

- In a background session the system runs the transfer; it continues even if the app is terminated by the system.
- Re-queuing the list on relaunch without checking duplicated a transfer that was in progress.
- Keeping the registry by task identifier stayed robust in the same relaunch.
- The order of the finish and queue operations determined the result; the system determines the order, and it was not measured.
- When the transfer finishes, a delegate receives the result, and the file must be moved from the temporary location.
- This lesson's sensitivity: **repeat-sensitive**.

## Next Step

The six lessons of the networking layer end here. The next topic moves to the concurrency layer and measures what running the same job twice, and interleaving two jobs, changes in a shared state.
