All articles
Mobile DevelopmentAugust 9, 2026 6 min read

Background Tasks in React Native 2026: Why iOS Kills Your Sync and How to Work Around It

Background execution is where cross-platform apps quietly fall apart. Here's what actually works on iOS and Android in 2026, and where you should stop fighting the OS.

Every few months a client asks us why their React Native app "stops syncing when the phone is in a pocket." The honest answer is that iOS and Android both actively fight against apps running in the background, and the abstractions we use in JavaScript hide just how aggressive that fight is. This is a field guide to what still works in 2026, what quietly doesn't, and where you should stop trying.

The mental model most teams get wrong

When a React Native developer says "background task," they usually mean one of four very different things:

  • Deferred one-shot work — finish uploading a file after the user backgrounds the app.
  • Periodic sync — pull new data every 15–30 minutes.
  • Event-driven wake-ups — react to a push, a geofence, a Bluetooth beacon, or a significant location change.
  • Long-running foreground-ish work — navigation, audio, fitness tracking.

iOS and Android treat these four categories completely differently, and the libraries in the Expo and React Native ecosystem paper over the differences badly. If you try to solve category 2 (periodic sync) using tools designed for category 3 (event-driven), you'll ship something that works in dev, passes QA, and silently dies on real user devices within a week of install.

Why iOS is stricter than you remember

iOS has been tightening background execution every release since iOS 13, and in 2026 the situation is roughly:

  • BGAppRefreshTask (what expo-background-fetch wraps) runs when the OS feels like it. On a lightly used app, that can mean never. Apple's own docs say "opportunistically," and in our experience that translates to somewhere between a few times a day and zero times a week depending on user behaviour.
  • Low Power Mode, Focus modes, and the "Background App Refresh" toggle in Settings can each independently disable your task.
  • If the user force-quits your app via the app switcher, background tasks are suspended until the app is launched again. This alone accounts for a huge share of "my sync doesn't work" bug reports.

Android is more permissive but has its own landmines: Doze, App Standby Buckets, and the OEM-specific killers (Xiaomi, Oppo, Samsung's aggressive battery optimiser) that will happily ignore WorkManager constraints on non-stock ROMs.

The 2026 toolbox for Expo and bare React Native

Here's what we actually reach for, and when.

expo-background-task (the new one)

Expo shipped expo-background-task to replace the old expo-background-fetch API, which is now deprecated. On iOS it uses BGTaskScheduler; on Android it uses WorkManager. The API is cleaner and lets you register tasks that survive app restarts.

import * as BackgroundTask from 'expo-background-task';
import * as TaskManager from 'expo-task-manager';

const SYNC_TASK = 'app.sync.pull';

TaskManager.defineTask(SYNC_TASK, async () => {
  try {
    const changed = await pullDeltas();
    return changed
      ? BackgroundTask.BackgroundTaskResult.Success
      : BackgroundTask.BackgroundTaskResult.Failed;
  } catch (e) {
    return BackgroundTask.BackgroundTaskResult.Failed;
  }
});

export async function registerSync() {
  const status = await BackgroundTask.getStatusAsync();
  if (status !== BackgroundTask.BackgroundTaskStatus.Available) return;

  await BackgroundTask.registerTaskAsync(SYNC_TASK, {
    minimumInterval: 15, // minutes — a hint, not a promise
  });
}

Use this for best-effort periodic sync. Do not use it for anything a user will notice being late. Treat any successful execution as a bonus, not a guarantee.

Silent push as a wake-up primitive

If you actually need the app to sync when something happens on your server, silent pushes are the right tool — with caveats. On iOS, set content-available: 1 and keep the payload minimal. Apple throttles silent pushes per app per device, and if you send too many, they get downgraded to "delivered when the OS feels like it," which defeats the purpose.

We covered the delivery failure patterns in our push notifications breakdown, but the short version: budget for roughly 2–3 silent pushes per hour per device on iOS as a safe ceiling, and always send a visible push as a fallback for anything time-sensitive.

WorkManager via a native module (Android-only concerns)

When we need Android-specific behaviour — say, sync only on unmetered networks with charging, retried with exponential backoff — we drop into a small Kotlin module wrapping WorkManager rather than fighting Expo's cross-platform abstraction.

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)
    .setRequiresCharging(true)
    .build()

val request = PeriodicWorkRequestBuilder<SyncWorker>(30, TimeUnit.MINUTES)
    .setConstraints(constraints)
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES)
    .build()

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "sync",
        ExistingPeriodicWorkPolicy.KEEP,
        request
    )

This is one of the honest cases where writing a bit of Kotlin is dramatically better than a cross-platform wrapper. The wrapper libraries either don't expose constraints properly or lag behind WorkManager releases.

expo-location background modes

Geofencing and significant-change location updates are the most reliable way to wake an iOS app in the background, because they're event-driven rather than opportunistic. If your product genuinely needs location awareness — delivery, field service, fitness — this is your friend. If it doesn't, do not add background location just to get sync execution. App Review will reject it, and rightly so.

The war story: a sync that only ran on Wednesdays

One of our clients — a B2B inventory app — reported that data would occasionally be 48 hours stale for a subset of iOS users. Every user was on the latest build, background refresh was enabled, and silent pushes were being sent.

What we found after a week of telemetry:

  1. The app was rarely opened by warehouse supervisors on weekends.
  2. iOS's BGTaskScheduler heuristics deprioritise apps the user doesn't launch.
  3. Silent pushes were being sent, but the payload was over 4KB (embedded a JWT and some metadata) and iOS was silently dropping them.
  4. When the user finally opened the app Monday morning, everything sync'd correctly — so bug reports blamed "Monday morning slowness."

The fix was three-part: shrink the silent push payload to under 1KB, add a visible "tap to refresh" banner when the last-sync timestamp exceeded 6 hours, and stop pretending background fetch was going to save us. Sync reliability jumped from around 60% to over 95% within a fortnight.

The lesson: on iOS, design for the case where the OS never runs your background task. If your UX only works when background sync fires, your UX is broken.

App Review pitfalls specific to background execution

A few things reviewers flag consistently in 2026:

  • Background location without a clear, visible user benefit. "To improve sync" is not a benefit. Reviewers want to see the feature that requires it, in the app, obviously.
  • UIBackgroundModes with audio or voip when the app isn't really an audio or VoIP app. People try this to keep the app alive. It gets rejected, sometimes with a developer account warning.
  • Background fetch that clearly does analytics work. If your background task is uploading events rather than fetching user-visible data, describe it honestly in the review notes.

On Android, the Play Console will flag apps that request SCHEDULE_EXACT_ALARM or FOREGROUND_SERVICE_* permissions without a matching declared use case. Since 2024 the declarations are stricter, and "background sync" is not a valid category for a foreground service.

What to test that your QA probably isn't testing

Standard QA on a fresh device with the app in the foreground will tell you nothing about background reliability. What actually catches bugs:

  • Leave the app installed but unopened for 72 hours, then check whether any background work fired (log to a remote endpoint from inside the task).
  • Force-quit the app on iOS and confirm your team knows that no background tasks will run until relaunch.
  • Toggle Low Power Mode and repeat the periodic-sync test.
  • On Android, test on at least one Xiaomi or Oppo device — the aggressive killers behave nothing like a Pixel.
  • Test with airplane mode toggled on and off during the task window, to make sure your retry logic doesn't wedge.

Where we'd start

If you're adding background work to a React Native app today, start by writing down which of the four categories you actually need. Nine times out of ten, teams reach for periodic background fetch when what they really want is a silent push with a visible fallback, or a pull-to-refresh with a smarter cache. Build for the case where the OS grants you zero background execution, then treat any successful background run as a small gift.

And if you find yourself writing native Kotlin or Swift to get around a limitation, that's usually the right call — the cross-platform tax on background execution is real, and paying it selectively beats fighting it everywhere. If you'd like a hand auditing the background behaviour of an existing app, our mobile team does exactly this kind of work.

#React Native#Expo#iOS#Android#Background Tasks

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project