URLSession: Common pitfalls with background download & upload tasks

URLSession enables you to download and upload from the background. Although the basics seem to be easy, it’s quite hard to do it right. From the available resources and documentation, there are quite a few small things which are really important to make background uploading work.

For my day to day job, I’m developing new features for the Collect by WeTransfer app. One of these features is background downloading and uploading in which both can also be triggered by an app extension. Although the first version was set up quite easily, it turned out to have quite a few pitfalls.

Note: In this blog post I’m going over some common pitfalls. If you’re looking for ways to set up background downloading and uploading, take a look at this documentation by Apple to kickstart your implementation. After that, of course, head back and read up on my learnings and save yourself from common pitfalls.

Each URLSessionConfiguration needs a unique identifier

This is mainly an issue if you’re triggering background downloads or uploads from an app extension. When your host app is relaunched with an identifier passed in the following method:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void)

You could just ignore the passed identifier and rebuild the URLSession as you would for your host app. However, with app extensions, you’re dealing with multiple URLSession instances. Each of those needs their own custom identifier and that identifier needs to be used on rebuilding your session as well.

I came to this conclusion after reading this documentation by Apple, stating:

Because only one process can use a background session at a time, you need to create a different background session for the containing app and each of its app extensions. (Each background session should have a unique identifier.) It’s recommended that your containing app only use a background session that was created by one of its extensions when the app is launched in the background to handle events for that extension. If you need to perform other network-related tasks in your containing app, create different URL sessions for them.

For us, this basically meant to create a new URLSession for any incoming identifier and cache that instance.

/// Contains any `URLSession` instances associated with app extensions.
private lazy var appExtensionSessions: [URLSession] = []

/// Creates an identical `URLSession` for the given identifier or returns an existing `URLSession` if it was already registered.
/// - Parameter identifier: The `URLSessionConfiguration` identifier to use for recreating the `URLSession`.
/// - Returns: A newly created or existing `URLSession` instance matching the given identifier.
private func session(for identifier: String) -> URLSession {
    if let existingSession = appExtensionSessions.first(where: { $0.configuration.identifier == identifier }) {
        return existingSession

    let configuration = URLSessionConfiguration.background(withIdentifier: identifier)
    configuration.sharedContainerIdentifier = appGroup
    let appExtensionSession = URLSession(configuration: configuration, delegate: self, delegateQueue: sessionOperationQueue)
    return appExtensionSession

Bundle based identifiers

To play it safe you can use the following code to make sure your URLSessionConfiguration identifier is always unique.

let appBundleName = Bundle.main.bundleURL.lastPathComponent.lowercased().replacingOccurrences(of: " ", with: ".")
let sessionIdentifier: String = "com.wetransfer.networking.\(appBundleName)"
let configuration = URLSessionConfiguration.background(withIdentifier: sessionIdentifier)

Don’t forget the shared container identifier

If you’re implementing background downloading and uploading inside an app extension you need to set the shared container identifier. As app extensions get terminated quickly, you need to make sure you’re performing those data tasks on a background URLSession. You can refer to Sharing Data with Your Containing App for guidance on setting up a shared container. The same link contains info on handling common scenarios in App Extensions and also covers downloading and uploading.

You can set the identifier as followed:

let configuration = URLSessionConfiguration.background(withIdentifier: "swiftlee.background.url.session")
configuration.sharedContainerIdentifier = "group.swiftlee.apps"

Only upload tasks from a file are supported

The basically means that you’ll have to upload from a file. Save the file locally first and start uploading from that file location. Uploads from data instances or a stream fail directly after the app exits. So keep an eye sharp here, as it might seem to work at first. However, we’re uploading in the background and that has to work!

The isDiscretionary parameter can really hurt

I remember myself sitting in front of my desk, staring at my screen. Downloads didn’t work until I plugged in the phone charger. After reading the documentation for almost every method and parameter, I encountered the `isDiscretionary` property:

For configuration objects created using the background(withIdentifier:) method, use this property to give the system control over when transfers should occur.

That already made me think. Reading on:

When transferring large amounts of data, you are encouraged to set the value of this property to true. Doing so lets the system schedule those transfers at times that are more optimal for the device. For example, the system might delay transferring large files until the device is plugged in and connected to the network via Wi-Fi.

And it totally made sense!

To be clear, it’s good to set this property to true in some cases. However, for our use case with the Collect app, we needed instant downloading and uploading. Therefore, we had to set the property back to it’s default value of false. It’s also important to point out that this property will be ignored if you’re creating non-background URLSession instances.


That’s it! A lot of my learnings are shared with you. Hopefully, it saves you time and a few headaches. Let me know your pitfalls so I can keep improving this article.