Background Tasks in React Native 2026: What iOS and Android Will Actually Run
Background execution is where cross-platform promises go to die. Here's what Expo, iOS, and Android actually let you run in 2026 — and the patterns that survive both OS schedulers and app review.
Every mobile team eventually files the same ticket: "the app should sync in the background." Then someone spends three sprints discovering that iOS runs your task when it feels like it, Android kills it the moment the user swipes the app away on a Xiaomi device, and the Expo docs are honest but easy to misread. Here's what actually works in 2026.
The mental model: you are not scheduling, you are suggesting
The single biggest mistake teams make is treating BackgroundFetch or WorkManager like setInterval. Neither OS gives you guaranteed execution. You are handing the system a request and a set of constraints — "run this when the device is on Wi-Fi, charging, and the user has opened the app recently" — and the scheduler decides whether, when, and how often to honour it.
On iOS, the heuristics are opaque and tied to usage patterns. An app the user opens every morning at 8am will get background fetch opportunities around 7:45am. An app they haven't opened in a week will effectively stop running in the background. Apple does not document the exact algorithm and it has shifted at least twice since iOS 15.
On Android, the system is more generous but fragmented. Stock Android and Pixel devices respect WorkManager constraints reasonably well. Samsung's One UI is stricter about Doze. Xiaomi, Oppo, and Vivo ship aggressive battery managers that will kill background work from any app not whitelisted by the user — and users almost never whitelist your app.
If your product requirement is "sync within 60 seconds of a server event," background tasks are the wrong tool. You want push notifications with a data payload, and you want to treat background execution as a nice-to-have.
What Expo gives you in 2026
The Expo SDK ships three relevant modules:
expo-task-manager— the registry layer. You define named tasks at the top level of your JS bundle.expo-background-task— the modern replacement forexpo-background-fetch, aligned with iOSBGTaskSchedulerand AndroidWorkManager.expo-notifications— for silent/data push, which is often the better answer.
The shift from expo-background-fetch to expo-background-task matters. The old API wrapped iOS's deprecated performFetchWithCompletionHandler, which Apple has been phasing out. The new module uses BGAppRefreshTask on iOS and WorkManager on Android, which is where the platforms are actually investing.
Minimum viable setup
import * as BackgroundTask from 'expo-background-task';
import * as TaskManager from 'expo-task-manager';
const SYNC_TASK = 'sync-pending-mutations';
TaskManager.defineTask(SYNC_TASK, async () => {
try {
const result = await syncQueue.flush({ timeoutMs: 25_000 });
return result.hadWork
? BackgroundTask.BackgroundTaskResult.Success
: BackgroundTask.BackgroundTaskResult.Success;
} catch (err) {
reportError(err);
return BackgroundTask.BackgroundTaskResult.Failed;
}
});
export async function registerBackgroundSync() {
const status = await BackgroundTask.getStatusAsync();
if (status !== BackgroundTask.BackgroundTaskStatus.Available) return;
await BackgroundTask.registerTaskAsync(SYNC_TASK, {
minimumInterval: 15, // minutes; a hint, not a promise
});
}
Two things trip people up here. First, defineTask must run at module load, not inside a component — otherwise the OS wakes your JS context, finds no handler registered, and marks the task as failed. Second, minimumInterval is a floor, not a ceiling. iOS might run you every six hours. Android might run you every 15 minutes on Wi-Fi and never on cellular.
iOS specifics: BGTaskScheduler, budgets, and the 30-second cliff
Under the hood, expo-background-task registers a BGAppRefreshTask identifier. You get roughly 30 seconds of wall-clock time to do your work before the system suspends you. If you exceed it, iOS marks the task as expired and your app's background budget shrinks — meaning fewer future opportunities.
In our experience, treating 20 seconds as the hard ceiling for all work (including network, decoding, and DB writes) keeps you in the system's good graces. If you need more time for an upload, use URLSession background transfers, which is a different system and not what expo-background-task handles. For large uploads from React Native you'll want a native module or something like expo-file-system's background upload hooks.
You also need to declare the task identifiers in Info.plist via your Expo config:
{
"expo": {
"ios": {
"infoPlist": {
"BGTaskSchedulerPermittedIdentifiers": [
"com.yourco.app.sync-pending-mutations"
],
"UIBackgroundModes": ["fetch", "processing", "remote-notification"]
}
}
}
}
If the identifier in your JS doesn't match the one in Info.plist, iOS silently refuses to schedule the task. There is no runtime error. You just... never run. Check with the LLDB breakpoint trick Apple documents for BGTaskScheduler during development — it's the only reliable way to force a background launch while debugging.
Android specifics: WorkManager and the OEM problem
On Android, expo-background-task enqueues a PeriodicWorkRequest through WorkManager. This is the right primitive. WorkManager survives app process death, reboots (if you request it), and respects Doze mode intelligently.
The problems start with OEM customisations. Our rough field data:
- Pixel / stock Android: tasks run close to the requested interval, give or take Doze maintenance windows.
- Samsung: generally reliable if the user has opened the app in the last few days.
- Xiaomi, Oppo, Vivo, Realme: unreliable without the user manually enabling "Autostart" and disabling battery optimisation.
There is no API to detect or fix this programmatically in a way that passes Play Store review. What we do instead:
- Detect the manufacturer via
expo-device. - On known-aggressive OEMs, show a one-time explainer screen with a deep link to the autostart settings when the user enables features that depend on background sync.
- Treat background tasks as best-effort and always fall back to foreground sync on app open.
Don't forget foreground services for real work
If you're doing anything user-visible or time-sensitive — tracking a run, uploading a video, playing audio — you need a foreground service, not a background task. These require a persistent notification and, since Android 14, a declared foregroundServiceType in your manifest. Google Play now rejects apps that misdeclare these types, and the review bar got stricter in 2025.
When push beats polling
For most "keep data fresh" use cases, silent push is a better primitive than periodic background tasks. You get:
- Server-triggered execution, so you only wake the app when there's actually new data.
- Better battery behaviour, because the device radio is already active for other push traffic.
- More predictable delivery on iOS, especially combined with a user-visible notification as a fallback.
The catch on iOS: silent pushes (content-available: 1 with no alert) are rate-limited and deprioritised. Apple's guidance is "two or three per hour" and we've seen heavier senders simply dropped. If freshness matters, send a visible notification. If you need both silent sync and a visible nudge, send them as separate pushes.
For the plumbing, see our deeper notes on cross-platform push setup under our mobile services and the blog archive — the FCM HTTP v1 and APNs token-auth setup is the same whether you're triggering sync or showing an alert.
A pragmatic architecture
What holds up in production:
- A durable local queue (SQLite or MMKV-backed) for pending mutations. Background tasks and foreground app opens both drain this queue through the same code path.
- Idempotent server endpoints keyed by a client-generated UUID, so retrying a half-finished sync never duplicates data.
- Silent push as the primary freshness mechanism, with
expo-background-taskas the belt-and-braces fallback. - A visible "last synced" timestamp in the UI. Users forgive stale data; they do not forgive silent lies.
- A manual pull-to-refresh, always. It's the escape hatch when the OS has decided your app doesn't deserve CPU today.
Where we'd start
If you're adding background sync to an existing React Native app today, don't start with expo-background-task. Start by making your sync code work correctly from a single foreground entry point — idempotent, resumable, observable. Then layer silent push on top of it, because that gives you server-controlled freshness with the best delivery characteristics on both platforms. Only then register a periodic background task as the third line of defence, and treat anything it manages to run as a bonus. Teams that build in this order ship. Teams that start with the background task spend six weeks debugging why their Xiaomi test device never syncs.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

App Store Review Rejections in 2026: The React Native Patterns That Keep Getting Flagged
A field report on the App Store and Play Store rejection patterns we see hit React Native and Expo apps most often in 2026, with concrete fixes before you resubmit.

Push Notifications in React Native 2026: Expo Notifications, FCM HTTP v1, and the APNs Gotchas
A practical breakdown of push on React Native in 2026: Expo Notifications vs raw FCM/APNs, token lifecycles, HTTP v1 migration pain, and the silent failures that eat your delivery rates.

EAS Update in 2026: A Field Guide to OTA Rollouts That Don't Brick Your App
OTA updates in React Native sound like a cheat code until you ship a bad JS bundle to 40,000 devices at 2am. Here's how we structure EAS Update channels, rollouts, and rollbacks so that never happens twice.
