Background Processing
Overview
Background execution is a privilege, not a right. iOS actively limits background work to protect battery life and user experience. Core principle: Treat background tasks as discretionary jobs β you request a time window, the system decides when (or if) to run your code.
Key insight: Most "my task never runs" issues stem from registration mistakes or misunderstanding the 7 scheduling factors that govern execution. This skill provides systematic debugging, not guesswork.
Energy optimization: For reducing battery impact of background tasks, see axiom-energy skill. This skill focuses on task mechanics β making tasks run correctly and complete reliably.
Requirements: iOS 13+ (BGTaskScheduler), iOS 26+ (BGContinuedProcessingTask), Xcode 15+
Example Prompts
Real questions developers ask that this skill answers:
1. "My background task never runs. I register it, schedule it, but nothing happens."
β The skill covers the registration checklist and debugging decision tree for "task never runs" issues
2. "How do I test background tasks? They don't seem to trigger in the simulator."
β The skill covers LLDB debugging commands and simulator limitations
3. "My task gets terminated before it completes. How do I extend the time?"
β The skill covers task types (BGAppRefresh 30s vs BGProcessing minutes), expiration handlers, and incremental progress saving
4. "Should I use BGAppRefreshTask or BGProcessingTask? What's the difference?"
β The skill provides decision tree for choosing the correct task type based on work duration and system requirements
5. "How do I integrate Swift 6 concurrency with background task expiration?"
β The skill covers withTaskCancellationHandler patterns for bridging BGTask expiration to structured concurrency
6. "My background task works in development but not in production."
β The skill covers the 7 scheduling factors, throttling behavior, and production debugging
Red Flags β Task Won't Run or Terminates
If you see ANY of these, suspect registration or scheduling issues:
- Task never runs: Handler never called despite successful
submit()
- Task terminates immediately: Handler called but work doesn't complete
- Works in dev, not prod: Task runs with debugger but not in release builds
- Console shows no launch: No "BackgroundTask" entries in unified logging
- Identifier mismatch errors: Task identifier not matching Info.plist
- "No handler registered": Handler not registered before first scheduling
Difference from energy issues
- Energy issue: Task runs but drains battery (see
axiom-energy skill)
- This skill: Task doesn't run, or terminates before completing work
Mandatory First Steps
ALWAYS verify these before debugging code:
Step 1: Verify Info.plist Configuration (2 minutes)
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.yourapp.refresh</string>
<string>com.yourapp.processing</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<array>
<string>fetch</string>
<string>processing</string>
</array>
Common mistake: Identifier in code doesn't EXACTLY match Info.plist. Check for typos, case sensitivity.
Step 2: Verify Registration Timing (2 minutes)
Registration MUST happen before app finishes launching:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
func someButtonTapped() {
BGTaskScheduler.shared.register(...)
}
Exception: BGContinuedProcessingTask (iOS 26+) uses dynamic registration when user initiates the action.
Step 3: Check Console Logs (5 minutes)
Filter Console.app for background task events:
subsystem:com.apple.backgroundtaskscheduler
Look for:
- "Registered handler for task with identifier"
- "Scheduling task with identifier"
- "Starting task with identifier"
- "Task completed with identifier"
- Error messages about missing handlers or identifiers
Step 4: Verify App Not Swiped Away (1 minute)
Critical: If user force-quits app from App Switcher, NO background tasks will run.
Check in App Switcher: Is your app still visible? Swiping away = no background execution until user launches again.
Background Task Decision Tree
Need to run code in the background?
β
ββ User initiated the action explicitly (button tap)?
β ββ iOS 26+? β BGContinuedProcessingTask (Pattern 4)
β ββ iOS 13-25? β beginBackgroundTask + save progress (Pattern 5)
β
ββ Keep content fresh throughout the day?
β ββ Runtime needed β€ 30 seconds? β BGAppRefreshTask (Pattern 1)
β ββ Need several minutes? β BGProcessingTask with constraints (Pattern 2)
β
ββ Deferrable maintenance work (DB cleanup, ML training)?
β ββ BGProcessingTask with requiresExternalPower (Pattern 2)
β
ββ Large downloads/uploads?
β ββ Background URLSession (Pattern 6)
β
ββ Triggered by server data changes?
β ββ Silent push notification β fetch data β complete handler (Pattern 7)
β
ββ Short critical work when app backgrounds?
ββ beginBackgroundTask (Pattern 5)
Task Type Comparison
| Type |
Runtime |
When Runs |
Use Case |
| BGAppRefreshTask |
~30 seconds |
Based on user app usage patterns |
Fetch latest content |
| BGProcessingTask |
Several minutes |
Device charging, idle (typically overnight) |
Maintenance, ML training |
| BGContinuedProcessingTask |
Extended |
System-managed with progress UI |
User-initiated export/publish |
| beginBackgroundTask |
~30 seconds |
Immediately when backgrounding |
Save state, finish upload |
| Background URLSession |
As needed |
System-friendly time, even after termination |
Large transfers |
Common Patterns
Pattern 1: BGAppRefreshTask β Keep Content Fresh
Use when: You need to fetch new content so app feels fresh when user opens it.
Runtime: ~30 seconds
When system runs it: Predicted based on user's app usage patterns. If user opens app every morning, system learns and refreshes before then.
Registration (at app launch)
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.yourapp.refresh",
using: nil
) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
return true
}
Scheduling (when app backgrounds)
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Failed to schedule refresh: \(error)")
}
}
func applicationDidEnterBackground(_ application: UIApplication) {
scheduleAppRefresh()
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .background {
scheduleAppRefresh()
}
}
Handler
func handleAppRefresh(task: BGAppRefreshTask) {
task.expirationHandler = { [weak self] in
self?.currentOperation?.cancel