Implementing Background Tasks for iOS App
iOS aggressively limits background code execution. An app that goes to background gets 30 seconds to finish current work, then is suspended. BackgroundTasks framework (iOS 13+) is the only official way to run code in background on a regular basis: data synchronization, content updates, ML inference.
Two Types of Tasks
BGAppRefreshTask — short task (30 seconds maximum). System launches it when conditions are right: device is charging or connected to Wi-Fi, user is active. Suitable for updating interface data.
BGProcessingTask — long task (minutes). Launched only when charging and on Wi-Fi. Suitable for heavy operations: database migration, large content download, CoreML model retraining.
Registration and Scheduling
In Info.plist add task identifiers to BGTaskSchedulerPermittedIdentifiers:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.refresh</string>
<string>com.yourapp.processing</string>
</array>
Registration in AppDelegate before applicationDidFinishLaunching completes:
import BackgroundTasks
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
self.handleRefresh(task: task as! BGAppRefreshTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.processing",
using: nil
) { task in
self.handleProcessing(task: task as! BGProcessingTask)
}
return true
}
Scheduling — when app transitions to background:
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // not earlier than 15 min
try? BGTaskScheduler.shared.submit(request)
}
func scheduleProcessing() {
let request = BGProcessingTaskRequest(identifier: "com.yourapp.processing")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = true
request.earliestBeginDate = Date(timeIntervalSinceNow: 3600)
try? BGTaskScheduler.shared.submit(request)
}
Schedule every time when going to background via sceneDidEnterBackground or applicationDidEnterBackground. One submit call — one task submission to queue.
Task Handler
func handleRefresh(task: BGAppRefreshTask) {
// Reschedule next execution
scheduleAppRefresh()
let syncTask = Task {
do {
try await DataSyncService.shared.sync()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
// Mandatory! If system takes CPU — cancel and signal
task.expirationHandler = {
syncTask.cancel()
task.setTaskCompleted(success: false)
}
}
expirationHandler is the most common mistake people skip. If you don't call setTaskCompleted before time expires, system kills the process and marks the app as abusing background execution. After several such cases, iOS stops launching the app's tasks.
When System Really Launches Tasks
Can't guarantee specific launch time — iOS decides based on usage patterns, battery, network. Apps used frequently get more background time. New or rarely used apps get less.
Debugging in Xcode: tasks can be force-launched via Xcode Debug Menu while debugging on real device:
Xcode → Debug → Simulate Background Fetch
Or via lldb:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
Simulator doesn't support BackgroundTasks — only real device.
URLSession Background Transfer
For background download and upload BackgroundTasks is not needed — use URLSessionConfiguration.background(withIdentifier:). System manages transfer itself, app gets callback on completion via AppDelegate.application(_:handleEventsForBackgroundURLSession:completionHandler:). This works even if app was unloaded from memory.
Timeline Benchmarks
| Task | Timeline |
|---|---|
| BGAppRefreshTask (data synchronization) | 1 day |
| BGProcessingTask (heavy operations) | 1–2 days |
| + URLSession background transfer | +0.5–1 day |
| Full background infrastructure | 2–3 days |







