Guarantees¶
Backgrounder.guarantees() returns a per-platform truth table. UX should branch on it rather than assume parity.
| Field | Android WorkManager |
iOS 18 BGTaskScheduler |
macOS 15 NSBackgroundActivityScheduler |
JVM (library coroutines) |
|---|---|---|---|---|
survivesProcessDeath |
true | true | false (in-process) | false (in-process) |
survivesReboot |
true | true | false | false |
survivesForceQuit |
true | false | false | false |
honoursWallClock |
approximate | false (hint only) | approximate | approximate |
supportsRetryBackoff |
true (native) | true (library-emulated) | true (library-emulated) | true (library-emulated) |
cancelsInFlight |
true | false | true | true |
minimumPeriodicInterval |
15 min | 15 min recommended | 1 sec | 1 sec |
maxConcurrentTasks |
unbounded-ish | ~1000 | unbounded-ish | unbounded-ish |
Read carefully:
survivesForceQuit = falseon iOS. The single most important caveat. See Force-quit caveat (iOS).- All survival flags are
falseon macOS and the JVM. Both schedule in-process —NSBackgroundActivitySchedulerregistrations and the JVM's coroutine jobs live in your process. When the process exits (quit, force-quit, reboot), every schedule goes with it, and nothing relaunches the app. Re-schedule from your app's init path at next launch. honoursWallClock = falseon iOS meansearliestBeginDateis a hint — the system can defer indefinitely based on opaque heuristics (battery state, usage patterns, Low Power Mode).cancelsInFlight = falseon iOS meansBackgrounder.cancel(taskId)only kills pending requests for scheduled work; a worker already executing on iOS finishes whatever it was doing.
Process death¶
- In-flight work gets to try to complete — the library never preemptively kills a running worker.
- The platform's own resilience is honoured — and never imitated. On Android, WorkManager may re-dispatch a worker whose process died mid-run; if Android can complete the work, it should — the library does not suppress that. iOS, macOS, and the JVM offer no such resilience, and the library adds none: a run that died with its process is never replayed. On iOS, a one-shot whose
BGTaskdied mid-run is additionally cleared from the state store at the nextstart()so its ghost can't block futureConflictPolicy.Keepschedules. - Scheduled-but-never-started work is not "interrupted". Where the platform persists it (Android WorkManager; iOS pending
BGTaskRequests), it may start later in a new process and complete normally. A periodic's schedule survives where the platform persists it; only the interrupted cycle is lost (see missed cycles). - Ephemeral work is purged at the next launch before anything can dispatch, on every platform — see the
ephemeralflag.
Branching UX on guarantees¶
val g = backgrounder.guarantees()
if (!g.survivesForceQuit) {
// iOS: educate the user.
showToast("Open the app daily so we can sync.")
}
if (!g.honoursWallClock) {
// iOS: don't promise a wall-clock cadence.
label.text = "Syncs throughout the day" // not "syncs every hour"
}
if (!g.cancelsInFlight) {
// iOS: a Cancel button only stops *future* runs, not the current one.
cancelButton.subtitle = "Stops future runs"
}
What's not in the table¶
isForeground— there's no notion of "foreground work" in v1 (AndroidsetForegroundlands asExecutionHint.LongRunningin v2).requiresChargingoutside Android — Android honoursWorkConstraints.requiresChargingnatively via WorkManager. On iOS / macOS / JVM the library has no charging probe, so the field is silently ignored. Workers that need charging should check insideexecute()and returnWorkResult.Retry.- Anything observability-related —
Scheduler.observe()is v2.
Network constraints — honoured everywhere¶
WorkConstraints.networkRequired is honoured on every platform, by different mechanisms:
- Android —
WorkManagerrefuses to dispatch the worker until the constraint is met. The OS holds it indefinitely. - iOS / macOS / JVM — a library-managed pre-execution gate, driven by
reachable, waits up tomin(5 s, budget / 4). On timeout the worker is short-circuited toWorkResult.Retryand the scheduler reschedules per the request'sBackoffPolicy.
NetworkRequirement.Unmetered is honoured against ReachabilityStatus.isDataMetered == false (wifi/ethernet) on all platforms — Android maps to NetworkType.UNMETERED; Apple checks the metered axis directly. The legacy "downgrade Unmetered to Any on iOS" behaviour is gone. See Recipes → Require a network connection.