Background Tasks in React Native and Expo: What Actually Runs in 2026
Background execution on iOS and Android is a minefield of OEM quirks, OS budgets, and silent task kills. Here's what survives in production React Native and Expo apps in 2026 — and what to stop trying.

Every few months a client asks us to build something "simple" — sync data every 15 minutes, or track a delivery driver's location with the app closed. Then we have the conversation: the OS does not care what your product manager wants. Background execution on mobile in 2026 is a negotiation with iOS and Android, not a feature you ship.
This is a field guide to what actually runs in the background in React Native and Expo apps today, what gets silently killed, and where we'd reach for native modules instead.
The mental model: you are not a desktop app
The single biggest reason background work fails is that engineers carry over web or desktop assumptions. On mobile, background execution is a budget, not a capability. iOS gives you opportunistic windows scored by user behavior. Android gives you constrained workers, foreground services with mandatory notifications, and a graveyard of OEM battery optimizers that will kill your process anyway.
A useful rule of thumb we've adopted internally:
If the task must run at a specific time, on the device, without the user opening the app — assume it won't, and design a server-side fallback.
Push notifications, server cron jobs, and silent pushes that wake the app for a few seconds are almost always more reliable than scheduling client-side work. Build the background path as an optimization, not as the source of truth.
What iOS will actually do in 2026
iOS 17 through 18 tightened the screws on BGAppRefreshTask and BGProcessingTask. The system uses on-device learning to decide who gets a slot — apps the user opens daily get refreshed often, apps they ignore get starved. There is no override. setMinimumBackgroundFetchInterval is a hint, nothing more.
What you reliably get:
- Silent pushes (
content-available: 1) that grant ~30 seconds of execution, rate-limited by APNs and by user engagement. - Background location updates if you've declared the
locationbackground mode and the user picked "Always Allow". - Audio, VoIP, and CallKit, which are their own world.
- Background tasks scheduled via
BGTaskScheduler, executed when the system feels like it.
What you do not get: guaranteed periodic execution, ever.
What Android will actually do in 2026
Android 14 and 15 made foreground services explicit about why they're running. You now declare a foregroundServiceType (location, dataSync, mediaPlayback, etc.) and Play Console reviews it. Lying gets your app pulled.
WorkManager is still the right primitive for deferrable work. Doze mode, App Standby Buckets, and OEM customizations (Xiaomi, Oppo, Huawei, Samsung's Device Care) will still kill you if the user hasn't opened the app in a while. We have seen production apps lose 30 – 60% of scheduled WorkManager jobs on certain Chinese OEMs in our deployments — your mileage will vary.
Expo's primitives, and where they break down
Expo wraps most of this with three modules worth knowing:
expo-task-manager— the registry for headless tasksexpo-background-fetch— periodic-ish workexpo-locationwith background updates
A typical background fetch setup looks like this:
import * as BackgroundFetch from 'expo-background-fetch';
import * as TaskManager from 'expo-task-manager';
const SYNC_TASK = 'background-sync';
TaskManager.defineTask(SYNC_TASK, async () => {
try {
const updated = await syncPendingChanges();
return updated
? BackgroundFetch.BackgroundFetchResult.NewData
: BackgroundFetch.BackgroundFetchResult.NoData;
} catch (err) {
return BackgroundFetch.BackgroundFetchResult.Failed;
}
});
export async function registerBackgroundSync() {
const status = await BackgroundFetch.getStatusAsync();
if (status !== BackgroundFetch.BackgroundFetchStatus.Available) return;
await BackgroundFetch.registerTaskAsync(SYNC_TASK, {
minimumInterval: 15 * 60, // seconds; iOS treats as hint
stopOnTerminate: false, // Android
startOnBoot: true, // Android
});
}
This is fine. It will also run much less often than 15 minutes on both platforms, and on iOS it may stop entirely if the user force-quits the app. expo-background-fetch cannot fix that — no library can.
Headless JS on Android
For location tracking, geofencing, or anything that needs to outlive the JS runtime, expo-task-manager runs your task in a headless JS context. The gotcha: your task function loads a fresh JS bundle, which means no Redux state, no React context, no in-memory cache. Anything you need has to come from AsyncStorage, MMKV, SQLite, or a fresh network call.
A pattern that has saved us repeatedly:
TaskManager.defineTask(LOCATION_TASK, async ({ data, error }) => {
if (error) return;
const { locations } = data as { locations: Location.LocationObject[] };
// Don't try to dispatch to a Redux store that doesn't exist here.
const queue = await loadQueueFromMMKV();
queue.push(...locations);
await saveQueueToMMKV(queue);
// Best-effort flush; fail silently.
flushToServer(queue).catch(() => {});
});
Queue locally, flush opportunistically. When the foreground app opens, it picks up the queue and reconciles. Never assume the headless task has network.
The four scenarios we get asked about most
1. "Sync user data every 15 minutes"
Don't. Use silent push notifications triggered by your backend when data actually changes. You get sync exactly when needed, the user's battery doesn't bleed, and you sidestep iOS's scheduler entirely. Fall back to a BackgroundFetch task for users who disable notifications, but treat it as best-effort.
2. "Track the driver's location until delivery is done"
This is one of the few cases where a true foreground service is the right answer. On Android, declare foregroundServiceType="location" and show a persistent notification. On iOS, use startLocationUpdatesAsync with the location background mode. Document in your Play and App Store listings exactly why you need it — both stores reject apps that request "Always" location without a clear delivery, fitness, or safety use case.
Expect review pushback. Have a screencast ready showing the in-app explanation and the OS prompt.
3. "Run an ML inference job overnight"
Android: WorkManager with a charging + idle constraint, exposed through a native module. Expo's background fetch is too short-lived. iOS: BGProcessingTask is the right primitive, also requires a native module today. If you need this in an Expo app, write a config plugin and a small native module — or accept that the work happens server-side.
4. "Geofencing for store promotions"
expo-location's geofencing works, but iOS limits you to 20 active regions per app and Android has its own quirks. Pre-filter on the server based on the user's last known city, then hand the device a small set of regions. We've seen apps try to register 200 geofences and wonder why none of them fire.
Debugging what you can't see
The worst part of background work is that it fails silently in production while working fine on your desk. A few habits that pay off:
- Log every task entry and exit to a local ring buffer, then ship it with the next foreground session. Sentry breadcrumbs work, but a dedicated diagnostic screen the user (or your support team) can open is gold.
- Track wake counts server-side. If your silent push wakes the app, have it ping a lightweight endpoint with a wake reason. You'll spot iOS throttling and OEM kills within days.
- Test with the app force-quit. This is the single most common mismatch between developer expectation and reality on iOS.
- Test on a real Xiaomi or Oppo device if you ship internationally. Pixel and Samsung do not represent the Android population.
For iOS specifically, the simulator lies. Use the Simulate Background Fetch debug menu on a real device, and pair it with Console.app filtered on your bundle ID.
When to drop down to native
React Native and Expo cover roughly 80% of what most apps need in the background. The other 20% — long-running uploads, complex audio session management, CarPlay or Android Auto, HealthKit background delivery, ActivityKit Live Activities — is faster to write in Swift or Kotlin and bridge a small interface back to JS.
If you're already on Expo, a local config plugin plus an Expo Module is the cleanest path. You keep managed workflow benefits and only escape the sandbox where you need to. We've shipped this pattern for clients building logistics and health apps, and it scales better than trying to bend expo-background-fetch into something it isn't.
Where we'd start
If you're staring at a background requirement on a new build, in order:
- Ask whether a server-triggered silent push solves it. Usually yes.
- If not, pick the one OS primitive that matches (foreground service, BGProcessingTask, geofencing) and design around its constraints — not against them.
- Use
expo-task-managerfor the JS surface, but treat headless tasks as stateless workers with local storage and best-effort networking. - Build diagnostics from day one. You will need them.
- Reserve native modules for the cases where Expo's wrappers genuinely don't reach.
If you'd rather hand the whole thing off, this is the kind of work our team does day-to-day — see our mobile development services for how we approach it.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Crash-Free Rate in React Native: Hitting 99.9% Without Going Native
How we pushed a React Native + Expo app from 99.2% to 99.9% crash-free sessions — what actually mattered, what was noise, and where Sentry, Hermes, and EAS fit in.

Biometric Auth in React Native 2026: Face ID, Passkeys, and the Keychain Traps
Face ID and fingerprint look simple from the product side. The keychain semantics, passkey fallbacks, and review-time edge cases are where React Native teams lose a week. Here's what we've learned shipping it.

Push Notifications in Expo and React Native: Why Yours Are Silently Failing in 2026
Push delivery looks fine in your dashboard but users swear they never got the notification. Here's what actually breaks push on iOS and Android in 2026, and how to debug it before support tickets pile up.
