iOS Background Refresh: Why Tasks Disappear and How to Keep Them Reliable
Background jobs that work in debug but fail in production usually suffer from one issue: no deterministic scheduling contract. iOS treats background runtime as a budget, not a guarantee.
Step 1: register one clear task identifier per workload
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.learnitfree.sync", using: nil) { task in
handleSync(task: task as! BGAppRefreshTask)
}
Step 2: schedule next run at completion, not only on launch
func scheduleNext() {
let req = BGAppRefreshTaskRequest(identifier: "com.learnitfree.sync")
req.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 30)
try? BGTaskScheduler.shared.submit(req)
}
Step 3: keep work idempotent and short
func runSync() async throws {
let pending = try await api.fetchChanges(since: lastCursor)
try store.apply(pending) // safe if retried
}
Pitfall
Treating background refresh as a timer. The OS can delay or skip runs if your task history looks expensive.
Verification
- Task logs show periodic execution across device lock/unlock cycles.
- Retries do not duplicate writes.
- App launch does not become required for next schedule.