← Back to the library
MobileApplied · 8 min

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 · platform

The 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

expo-background-fetch, iOS background fetch (setMinimumBackgroundFetchInterval)expo-background-task on BGTaskScheduler and WorkManager
AlarmManager plus a long-running ServiceWorkManager (expedited or foreground work when the user is waiting)
beginBackgroundTask for long user-started jobs on iOSAlso BGContinuedProcessingTask on iOS 26+, with progress shown to the user

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

SOURCES

Guide reviewed