All articles
Mobile DevelopmentMay 21, 2026 7 min read

Background Tasks on React Native in 2026: What Actually Runs on iOS and Android

Background execution is the swampiest part of mobile. Here's what actually works on iOS and Android in 2026, what Expo gives you, and where you still need a native module.

Background Tasks on React Native in 2026: What Actually Runs on iOS and Android

Every few months a client asks us some version of the same question: "Can the app just sync in the background?" The honest answer in 2026 is still "sort of, sometimes, depending on the OS mood." Here's how we actually ship background work on React Native and Expo today, and where we stop fighting the platform.

Why background tasks are still hard

Both Apple and Google have spent the last five years tightening background execution. iOS treats it as a battery and privacy concern; Android treats it as a battery and OEM-fragmentation concern. The result is two completely different mental models, and a JavaScript runtime sitting on top trying to pretend they're the same.

A few things that haven't changed:

  • iOS will not let you run code on a schedule you control. You ask, the system decides.
  • Android will let you run code on a schedule, but Doze, App Standby, and aggressive OEM killers (Xiaomi, Oppo, Vivo, Huawei) can silently break it.
  • A JS bridge cold-start in the background costs real wall-clock time, and iOS gives you about 30 seconds total before it kills the process.

If your product spec says "the app must update every 15 minutes," push back before you write a line of code. That's not a thing you can promise on a phone.

The four categories of background work

We sort every request into one of these buckets before picking a tool. It saves a lot of arguing later.

  1. Deferred, opportunistic sync — "refresh the inbox sometime in the next few hours." This is what BGAppRefreshTask and WorkManager are built for.
  2. Triggered by an external event — a push lands, a geofence trips, a Bluetooth peripheral connects. The OS wakes you; you do a small amount of work.
  3. User-initiated long-running work — a file upload that should survive the user backgrounding the app. iOS has URLSession background uploads; Android has foreground services.
  4. Continuous monitoring — location tracking, fitness, audio. Different rules, different entitlements, different review risk. Not what this article is about.

Most "background sync" features people ask for are category 1, and category 1 is the one with the loosest guarantees.

What Expo gives you out of the box

As of Expo SDK 52 and 53, the relevant modules are expo-task-manager, expo-background-fetch, expo-background-task (the newer iOS-friendly wrapper around BGTaskScheduler), and expo-notifications for push-triggered work. They cover roughly 80% of category 1 and 2 use cases without writing Swift or Kotlin.

Where they fall short:

  • Background uploads that need to survive process death (category 3).
  • Anything that needs a foreground service notification on Android with custom UI.
  • iOS background processing tasks longer than a few seconds, where you need BGProcessingTask with charging/network constraints.

For those, you're writing a config plugin or going bare. That's fine — Expo prebuild made this much less painful than the old eject ritual.

iOS: BGTaskScheduler is the only game in town

On iOS, the modern API is BGTaskScheduler, exposed through expo-background-task. You register task identifiers in Info.plist, then submit a request. The system runs your task when it feels like it — usually when the device is charging, on Wi-Fi, and the user has actually opened your app recently.

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

const SYNC_TASK = 'app.inbox-sync';

TaskManager.defineTask(SYNC_TASK, async () => {
  try {
    await syncInbox();
    return BackgroundTask.BackgroundTaskResult.Success;
  } catch {
    return BackgroundTask.BackgroundTaskResult.Failed;
  }
});

export async function registerSync() {
  await BackgroundTask.registerTaskAsync(SYNC_TASK, {
    minimumInterval: 15 * 60, // 15 min, treated as a hint
  });
}

Things that will bite you:

  • minimumInterval is a hint. In our experience, on a healthy device with the app used daily, you'll see it fire a few times a day. On a device where the user opened the app once a week, you might see it fire roughly never.
  • Testing requires the LLDB trick: pause the debugger and call e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.inbox-sync"]. There is no friendly button.
  • TestFlight builds behave differently from debug builds. Always verify against a TestFlight or release build before you tell a stakeholder it works.

If you need guaranteed delivery of new data, stop using background fetch and use a push notification with content-available: 1. That's the lever Apple actually wants you to pull.

Android: WorkManager, and the OEM problem

Android is more permissive but more fragmented. WorkManager is the right tool for category 1 work, and expo-background-task wraps it on Android too. Constraints (network, charging, idle) work well. Periodic work has a 15-minute minimum interval that the system actually respects most of the time on AOSP-flavored devices.

The catch: the bottom half of the Android market runs OEM skins that kill background work aggressively to win battery benchmarks. Don't Kill My App (dontkillmyapp.com) keeps a current list. If your user base skews toward those devices, your WorkManager jobs will run on Pixel and Samsung and disappear on a Xiaomi Redmi until the user opens the app.

Mitigations we use, in order of preference:

  1. Stop relying on background work. Push the data with FCM and let the notification be the trigger.
  2. Foreground service for genuinely user-initiated work (an active upload, a download in progress). Requires the right manifest declaration and, since Android 14, a foregroundServiceType.
  3. In-app prompts asking the user to disable battery optimization for the app. This is a last resort and you should justify it to the user in plain language.

A note on foreground services

Foreground services are not a workaround for "I want my app to do whatever it wants." Google Play review will reject apps that declare dataSync or specialUse foreground services without a clear, user-visible reason. We've seen rejections climb sharply through 2025, particularly for apps that started a dataSync service on launch and never stopped it.

If you need a foreground service, write the review note before you write the code. If you can't justify it in two sentences to a reviewer, you don't have a valid use case.

Push-triggered background work

This is the pattern we reach for most often, and it works on both platforms with the same mental model:

  • iOS: send a push with content-available: 1 and optionally mutable-content: 1. Your app gets up to 30 seconds in didReceiveRemoteNotification (or a Notification Service Extension for mutable-content) to fetch data and update local state.
  • Android: send a data-only FCM message. Your app's background handler runs. No 30-second cliff, but you still want to be quick.

In Expo, the JS-side handler is registered via Notifications.registerTaskAsync plus a TaskManager.defineTask. It works, with two caveats:

  • On iOS, silent pushes are rate-limited by APNs. Apple's documentation says "no more than two or three per hour" on average and we've seen drops well below that on low-power-mode devices.
  • On Android, a data-only message must finish quickly or the OS gives up on you. If you need longer work, enqueue a WorkManager job from inside the handler.

What we actually ship

For a typical messaging or content app, our default stack is:

  • Primary update path: silent push (content-available / data-only FCM) triggers a small fetch.
  • Backup path: expo-background-task with a 15-minute hint, so the app gets a chance to catch up when the user hasn't opened it in a while.
  • User-visible long work: foreground service on Android, URLSession background config on iOS, both wrapped in a small native module behind a single JS API.
  • No promises in the spec. The product copy never says "updates every 15 minutes." It says "keeps your inbox fresh."

We also instrument every background entry point. A simple counter — task fired, task succeeded, task duration — sent to your analytics on next foreground gives you ground truth about whether your background work is actually running for real users. Without that data you're guessing, and guessing about background execution is how features ship broken.

Where we'd start

If you're adding background work to an existing React Native app this quarter, do this in order:

  1. Write down which of the four categories your feature is. If it's category 1, plan for "sometimes."
  2. Add silent push as the primary mechanism. Get FCM and APNs right before you touch task schedulers.
  3. Layer expo-background-task on top for opportunistic catch-up.
  4. Instrument everything. Ship. Look at the numbers after a week on real devices.
  5. Only then decide if you need to drop to native for a foreground service or background URLSession.

If you'd rather not work all this out from first principles, our mobile team has shipped this pattern in production apps and we're happy to compare notes.

#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