Background sync stops or never runs
“The periodic sync works while I'm testing, but in the field it runs hours late, only overnight, or not at all once people close the app.”
ERROR CODES YOU MAY SEE
A code is a clue. Use its meaning and surrounding evidence to narrow the cause.
REASON_USER_REQUESTEDAndroid ApplicationExitInfo · platformThe process was killed at the user's request, for example Force stop in Settings or removing the app from Recents (constant value 10, API 30+).
If scheduled work stopped on a device and the last exit reason is this, check whether it was a Force stop (which cancels the app's jobs and alarms until it is opened again) or a Recents swipe, which on some OEM builds behaves the same way.
Official reference for REASON_USER_REQUESTED (opens in new tab)Code definition reviewed
THE PRINCIPLE
Background execution is a hint, not a contract. Make every background job idempotent and resumable, and never let correctness depend on it running on time.
FIRST MOVES
- List what the job must guarantee. Anything user-visible and urgent belongs in a push or a user-initiated task; periodic background runs are for opportunistic prefetch and cleanup.
- iOS: the task identifier must be in BGTaskSchedulerPermittedIdentifiers, the handler registered before launch finishes, and the next request resubmitted from inside the handler. Expect ~30 s for app refresh, set an expiration handler, and remember that a user swipe-kill stops scheduling until the next launch.
- Android: use WorkManager with constraints rather than alarms or long-lived services. Periodic work has a 15-minute minimum and is deferred during Doze and in rarer standby buckets; test with adb shell dumpsys deviceidle force-idle and am set-inactive.
- Check ApplicationExitInfo for REASON_USER_REQUESTED and OEM battery settings before suspecting your code: a force-stopped app gets no jobs, alarms or FCM until it is opened again.
- Catch up on foreground: run the same sync on app start/resume, so a skipped background run only means slightly older data.
TOOLS: THEN → NOW
PATTERN SNAPSHOT
import * as BackgroundTask from "expo-background-task";
import * as TaskManager from "expo-task-manager";
const SYNC_TASK = "outbox-sync";
// Must be defined at module scope, not inside a component.
TaskManager.defineTask(SYNC_TASK, async () => {
const ok = await syncOutbox({ budgetMs: 20_000 }); // idempotent, resumable
return ok ? BackgroundTask.BackgroundTaskResult.Success
: BackgroundTask.BackgroundTaskResult.Failed;
});
// A hint: Android enforces >= 15 min; iOS chooses when (often overnight).
await BackgroundTask.registerTaskAsync(SYNC_TASK, { minimumInterval: 60 });CLOSE THE AI. EXPLAIN THIS.
What happens to a scheduled iOS background task after the user swipes the app away, and how should the app compensate?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
- Expo BackgroundTask (expo-background-task) (opens in new tab)Checked
- Apple: Choosing background strategies for your app (opens in new tab)Checked
- Android Developers: Optimize for Doze and App Standby (opens in new tab)Checked
- Android Developers: Persistent work with WorkManager (opens in new tab)Checked
Guide reviewed