All articles
Mobile DevelopmentJuly 11, 2026 6 min read

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

Background execution is where React Native apps quietly break. Here's what iOS and Android actually let you run in 2026, and how we ship features that survive Doze, low-power mode, and app review.

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

Background execution is where React Native apps quietly break. A feature works on the simulator, ships to TestFlight, and two weeks later a support ticket lands: "the sync stopped running overnight." The truthful answer is almost never a bug in your code — it's that iOS and Android decided your task wasn't worth the battery.

Here's what actually runs in the background on both platforms in 2026, what Expo gives you out of the box, and where we still drop to native.

The mental model most teams get wrong

Developers coming from web assume "background" means "my code keeps running when the app is closed." On mobile, that is almost never true. What you actually get are a handful of narrow, OS-mediated windows where the system decides to wake your process, gives you a budget (usually 30 seconds to a few minutes), and kills you if you overrun.

On iOS the windows are:

  • Background fetch via BGAppRefreshTask — short, opportunistic, minutes to hours between runs.
  • Background processing via BGProcessingTask — longer (minutes), but only when the device is charging and idle.
  • Silent push — a content-available: 1 payload that wakes the app briefly. Rate-limited hard by APNs.
  • Continuous modes — location, audio, VoIP, BLE. Each requires an entitlement and an App Review justification.

On Android the model is different. Since Doze and App Standby Buckets, everything funnels through:

  • WorkManager for deferrable, guaranteed work.
  • Foreground services for anything the user should see (with a persistent notification and, since Android 14, a declared foregroundServiceType).
  • AlarmManager for exact timing, now heavily restricted.
  • Firebase high-priority messages for wake-and-do-something-now.

If you internalise one thing: iOS decides when your task runs. Android decides how long your task lives. Design accordingly.

What Expo gives you in 2026

Expo covers the common cases without ejecting, and the API surface has stabilised nicely. The three modules that matter:

  • expo-task-manager — the registry. You define named tasks at the top level of your JS bundle.
  • expo-background-fetch — wraps BGAppRefreshTask on iOS and a periodic WorkManager job on Android.
  • expo-location — the only sane path to background location for most teams.

A minimal periodic sync looks like this:

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

const SYNC_TASK = 'app.sync.pending-writes';

TaskManager.defineTask(SYNC_TASK, async () => {
  try {
    const changed = await flushPendingWrites();
    return changed
      ? BackgroundFetch.BackgroundFetchResult.NewData
      : BackgroundFetch.BackgroundFetchResult.NoData;
  } catch {
    return BackgroundFetch.BackgroundFetchResult.Failed;
  }
});

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

  await BackgroundFetch.registerTaskAsync(SYNC_TASK, {
    minimumInterval: 15 * 60, // seconds — a hint, not a promise
    stopOnTerminate: false,
    startOnBoot: true,
  });
}

Two things developers miss here:

  1. defineTask must run at module scope, not inside a component. If the OS wakes your app headless, no React tree is mounted.
  2. minimumInterval is a floor, not a schedule. On iOS you'll typically see runs every 1 – 6 hours in our experience, and only if the user opens the app regularly. Rarely-used apps get demoted and effectively stop firing.

When background fetch is enough

If your feature is "refresh a feed occasionally," "upload queued analytics," or "pre-warm a cache before the user opens the app," background fetch is fine. Don't over-engineer it.

If your feature is "the user's data must be synced within 60 seconds of a server event," background fetch is the wrong tool. You want a data-only push.

Push-driven background work

A silent push is the closest thing to "call my code from the server." On iOS you send an APNs payload with content-available: 1 and no alert. On Android you send an FCM message with no notification block, only data.

The honest limits, based on shipping this across several client apps:

  • iOS throttles silent pushes aggressively. Apple's docs say "a few per hour." In practice, low-priority apps get delivered on the system's schedule, which can mean batching or dropping. Never use silent push for anything the user is waiting on.
  • Android delivers data messages while the app is in the background, but only if you set priority: high and the app isn't in a restricted App Standby Bucket. High priority also puts you on the hook for battery, and Google will flag abuse.
  • Force-quit on iOS kills silent push delivery entirely. There is no workaround. This is the single biggest cause of "why didn't my sync run" tickets.

A reasonable rule: use push to invalidate a cache and let the next foreground open do the real work. Don't try to do 20 seconds of network I/O in a silent push handler.

Background location: the one place we still go native

expo-location with startLocationUpdatesAsync handles most background location cases well, including the UIBackgroundModes plumbing and the Android foreground service. For fitness, delivery, or field-ops apps, though, we usually reach for a native module or a config plugin around a maintained library.

Why:

  • iOS significant-change and region monitoring have subtle wake behaviours that the Expo wrapper flattens.
  • Android 14+ requires a specific foregroundServiceType (location) and a runtime justification in your Play listing. Getting the manifest right matters for review.
  • Battery tuning (distance filters, accuracy downgrades when stationary) usually needs native-level control.

If you're on Expo and you need this, write a config plugin. Do not eject the whole app for one screen.

Android 14 and 15 gotchas

A few things that have burned us on recent Play submissions:

  • FOREGROUND_SERVICE_LOCATION must be declared explicitly. The generic FOREGROUND_SERVICE permission is not enough.
  • Exact alarms (SCHEDULE_EXACT_ALARM) now require user opt-in on Android 14+. Assume it's off and design around WorkManager.
  • If you target Android 15, foreground services started from the background have new restrictions. Trigger them from a user-visible action or from a high-priority FCM message, not from a timer.

App Review pitfalls

Background modes get scrutinised more than any other entitlement. The rejections we see most often:

  • "Background location not clearly justified." Your Info.plist strings need to describe why you need location when the app isn't open, in the specific product context. Generic strings get rejected.
  • VoIP background mode used for non-VoIP work. Apple has been actively removing this workaround since 2020. Don't try it.
  • Silent push used as a keepalive. If your app sends itself pushes just to stay warm, you'll get flagged.
  • Android foreground service without a persistent notification, or with a notification the user can dismiss. Play Console flags this on automated review now.

When in doubt, over-explain in the review notes. Include a short screencast of the background feature actually being used. It cuts a review cycle almost every time.

Testing background work without losing a week

Two tricks that shortened our iteration loop significantly:

Force an iOS background task to run

With the app paused in Xcode, use the debugger console:

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.sync.pending-writes"]

This fires your BGTask immediately instead of waiting for iOS to feel like it. Match the identifier to what you registered.

Force a WorkManager job on Android

adb shell cmd jobscheduler run -f <your.package.name> <job-id>

Or trigger Doze conditions directly:

adb shell dumpsys deviceidle force-idle
adb shell dumpsys deviceidle step

Run through the states and watch what your task actually does when the device thinks it's in a drawer overnight.

Where we'd start

If you're adding background work to a React Native app today:

  1. Write down the user-visible outcome first. "Data is fresh when the user opens the app" is a very different problem from "the server can push a state change within a minute."
  2. Default to expo-background-fetch + a silent push for invalidation. That combination covers 80% of real product needs without any native code.
  3. Only add a foreground service or continuous background mode when you can defend it to a reviewer in one sentence.
  4. Instrument every task with a lightweight server ping on start and finish. You cannot debug what you can't see, and users will not tell you when a background sync silently stopped firing three weeks ago.

If you want a second pair of eyes on a background architecture before it hits review, that's the kind of thing our mobile team does regularly — usually cheaper than one rejected submission.

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

Want a team like ours?

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

Start a project