Use beginBackgroundTask the Right Way
Working with background tasks has always been one of the more challenging parts of iOS development. Today, we’ll dive into UIApplication.beginBackgroundTask, discuss how it works, highlight common mistakes, and cover important considerations for using it safely and effectively.
What is beginBackgroundTask?Permalink
When the user presses the Home button or switches to another app, iOS moves your app to the background. By default, the app gets only a few seconds before the system suspends it entirely — meaning your code stops executing.
beginBackgroundTask(withName:expirationHandler:) is a UIApplication API that lets you tell the system: “I’m not done yet, please give me more time.” The system grants roughly 30 seconds of additional execution time (the exact amount is not guaranteed and may vary).
let taskID = UIApplication.shared.beginBackgroundTask(withName: "FinishUpload") {
// Expiration handler — called if time runs out before you end the task
UIApplication.shared.endBackgroundTask(taskID)
}
// Do your work here...
UIApplication.shared.endBackgroundTask(taskID)
The API returns a UIBackgroundTaskIdentifier which you use later to signal that the work is complete via endBackgroundTask(_:).
Bad Practices to AvoidPermalink
1. Not Calling endBackgroundTask When the Work Is DonePermalink
This is the most common mistake. Every call to beginBackgroundTask must be matched with a call to endBackgroundTask. If you forget, the system keeps the background assertion alive until the expiration handler fires and eventually terminates your app.
Wrong:
func uploadData() {
let taskID = UIApplication.shared.beginBackgroundTask(withName: "Upload") {
UIApplication.shared.endBackgroundTask(taskID)
}
networkService.upload(data) { result in
// Forgot to call endBackgroundTask here!
handleResult(result)
}
}
Right:
func uploadData() {
var taskID = UIBackgroundTaskIdentifier.invalid
taskID = UIApplication.shared.beginBackgroundTask(withName: "Upload") {
UIApplication.shared.endBackgroundTask(taskID)
}
networkService.upload(data) { result in
handleResult(result)
UIApplication.shared.endBackgroundTask(taskID) // Always end it
}
}
A safe pattern is to end the task in both your completion handler and your expiration handler — this covers both the happy path and the timeout path.
2. Not Naming Your Background TaskPermalink
beginBackgroundTask(expirationHandler:) — without a name — is the older variant. Always prefer beginBackgroundTask(withName:expirationHandler:) and pass a meaningful name.
Wrong:
let taskID = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(taskID)
}
Right:
let taskID = UIApplication.shared.beginBackgroundTask(withName: "SyncUserProfile") {
UIApplication.shared.endBackgroundTask(taskID)
}
The name shows up in Xcode’s Energy Log and in crash reports, which makes debugging significantly easier. When you have multiple background tasks running simultaneously, named tasks are the only way to tell them apart in diagnostics.
3. Assuming the Task Will Never Be SuspendedPermalink
beginBackgroundTask gives you extra time, but it does NOT guarantee your app will keep running. The system can still suspend or terminate your app under memory pressure, during a low-power event, or when the expiration handler fires and you haven’t ended the task.
Do not write code that assumes the background execution will always complete:
Wrong assumption:
// Kicking off a multi-step sync and assuming it will always finish
let taskID = UIApplication.shared.beginBackgroundTask(withName: "FullSync") {
UIApplication.shared.endBackgroundTask(taskID)
}
performStep1()
performStep2()
performStep3() // The app may be suspended before reaching here
UIApplication.shared.endBackgroundTask(taskID)
The expiration handler is your last chance to clean up, not a guarantee of completion.
4. Not Saving Work to DiskPermalink
Because background execution can be cut short at any moment, any critical state that lives only in memory can be lost. Always persist your progress to disk so that when the app relaunches, it can pick up where it left off.
var taskID = UIBackgroundTaskIdentifier.invalid
taskID = UIApplication.shared.beginBackgroundTask(withName: "SyncMessages") {
// Time is up — save whatever we have so far
saveProgressToDisk()
UIApplication.shared.endBackgroundTask(taskID)
}
syncMessages { progress in
// Checkpoint: persist progress incrementally
saveProgressToDisk()
} completion: {
clearSavedProgress()
UIApplication.shared.endBackgroundTask(taskID)
}
When the app relaunches (either by the user or via a background fetch), check for saved progress and resume from there:
func applicationDidFinishLaunching(_ application: UIApplication) {
if let savedProgress = loadProgressFromDisk() {
resumeSync(from: savedProgress)
}
}
This checkpoint-and-resume pattern is the backbone of robust background work on iOS.
5. Calling beginBackgroundTask Too Late — Inside applicationDidEnterBackgroundPermalink
A very common mistake is waiting until applicationDidEnterBackground to begin a background task. By that point, the app is already transitioning to the background, and the system may suspend you before your task even gets started properly.
Wrong:
func applicationDidEnterBackground(_ application: UIApplication) {
// Too late — the app is already being suspended
let taskID = UIApplication.shared.beginBackgroundTask(withName: "LateTask") {
UIApplication.shared.endBackgroundTask(taskID)
}
performCriticalWork()
UIApplication.shared.endBackgroundTask(taskID)
}
Right — start the task earlier, before backgrounding:
// In your view controller or service, as soon as the work begins
func startUpload() {
var taskID = UIBackgroundTaskIdentifier.invalid
taskID = UIApplication.shared.beginBackgroundTask(withName: "UploadTask") {
cancelUpload()
UIApplication.shared.endBackgroundTask(taskID)
}
networkService.upload(data) { _ in
UIApplication.shared.endBackgroundTask(taskID)
}
}
The intent of beginBackgroundTask is to extend time for work that is already in progress when the user backgrounds the app. Start it at the point where the work begins, not as a reaction to backgrounding.
If you do need applicationDidEnterBackground as a trigger, use it only to checkpoint state, not to kick off new long-running work.
ConclusionPermalink
beginBackgroundTask is a simple API, but the subtleties around it are what trips most developers up. Here’s a quick summary of what to keep in mind:
- Always pair
beginBackgroundTaskwithendBackgroundTask— in both the success path and the expiration handler. - Name every task — it costs nothing and makes debugging much easier.
- Don’t rely on the task completing — treat it as best-effort, not guaranteed.
- Persist progress to disk — so a relaunch can resume cleanly.
- Start early — begin the task when the work starts, not after the app has already entered the background.
Used correctly, beginBackgroundTask is a reliable safety net for finishing in-flight network requests, saving data, or completing short operations. Used incorrectly, it quietly drains battery, causes subtle bugs, and can lead to app terminations that are hard to reproduce.
Happy reading!
Like what you're reading?
If this article hit the spot, why not subscribe to the weekly newsletter?
Every weekend, you'll get a short and sweet summary of the latest posts and tips—free and easy, no strings
attached!
You can unsubscribe anytime.