mobile-background-tasks · git:20260709.eeb9e6c · 2026-07-09 · sha256 83adfeefb404cfde
mobile-background-tasks git:20260709.eeb9e6cA
Immutable. This exact content is served forever at /api/v1/blob/83adfeefb404cfde.
---
name: mobile-background-tasks
description: Background fetch, processing tasks, background location, headless JS, battery optimization - Expo and bare React Native
---
# React Native Background Tasks
> **Quick Guide:** Background tasks in React Native are heavily constrained by OS power management. Use `expo-background-task` (Expo) or `react-native-background-fetch` (bare RN) for periodic fetch. Use `expo-location` for background location tracking. iOS gives ~30s for refresh tasks (BGAppRefreshTask) and several minutes for processing tasks (BGProcessingTask). Android enforces 15-minute minimum intervals via WorkManager and restricts execution in Doze mode. Always call `finish()` or return a result when done -- the OS will terminate tasks that exceed their time budget.
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST define tasks in the top-level scope (global) -- tasks defined inside React components or lifecycle methods will NOT be registered when the app starts from the background)**
**(You MUST call `finish(taskId)` or return a `BackgroundTaskResult` when task execution completes -- failing to signal completion causes the OS to penalize or kill your app)**
**(You MUST request background permissions explicitly on both platforms -- iOS requires Info.plist UIBackgroundModes entries, Android requires manifest permissions)**
**(You MUST handle the OS killing your task at any time -- use expiration listeners on iOS and timeout callbacks on Android to clean up gracefully)**
**(You MUST keep background work minimal -- sync only changed data, avoid heavy computation, respect the ~30s iOS refresh limit)**
</critical_requirements>
---
**Auto-detection:** expo-task-manager, expo-background-task, expo-background-fetch, expo-location background, react-native-background-fetch, BackgroundFetch, TaskManager, defineTask, registerTaskAsync, startLocationUpdatesAsync, Headless JS, registerHeadlessTask, BGTaskScheduler, WorkManager, background fetch, background processing, background location
**When to use:**
- Syncing data periodically while the app is backgrounded (new messages, feeds, email)
- Tracking location in the background (fitness, delivery, navigation)
- Running periodic cleanup or maintenance tasks (cache purge, log upload)
- Keeping local data fresh so the app opens with current content
- Processing uploads or downloads that continue after backgrounding
**When NOT to use:**
- Real-time updates that need sub-second latency (use push notifications + foreground handling)
- Continuous audio playback (use the audio background mode, not task scheduling)
- Tasks that must execute at an exact time (OS scheduling is advisory, not precise)
- Tasks requiring more than a few minutes of CPU (iOS will terminate them)
**Key patterns covered:**
- Expo background tasks: `expo-background-task` (new) and `expo-background-fetch` (legacy)
- Bare RN background fetch: `react-native-background-fetch` with configure/scheduleTask
- Background location tracking with `expo-location` and TaskManager
- Android Headless JS for post-termination task execution
- iOS BGTaskScheduler constraints (refresh ~30s vs processing ~minutes)
- Android battery optimization: Doze mode, App Standby, WorkManager guarantees
- Task registration, unregistration, and lifecycle management
**Detailed Resources:**
- [examples/core.md](examples/core.md) - Expo background task, bare RN background fetch, background location, headless JS
- [reference.md](reference.md) - Decision frameworks, platform constraints, permission checklists
---
<philosophy>
## Philosophy
Background execution on mobile is a **privilege, not a right**. Both iOS and Android aggressively limit what apps can do in the background to preserve battery life and user experience. The OS decides when (and whether) your task runs -- you can only request execution and set minimum intervals.
**Core principles:**
1. **Minimize background work** -- Sync only deltas, not full datasets. The less you do, the more reliably the OS will schedule you.
2. **Always signal completion** -- Return a result code or call `finish()`. The OS tracks your task duration and penalizes apps that don't complete promptly.
3. **Define tasks globally** -- Background tasks must be registered at the top-level scope because the app may launch directly into background mode with no UI.
4. **Plan for termination** -- The OS can kill your task at any time. Use expiration/timeout handlers to save partial progress.
5. **Test on real devices** -- iOS simulators do not run BGTaskScheduler tasks. Android emulators may not enforce Doze mode.
6. **Respect platform differences** -- iOS kills all background tasks when the user force-quits. Android Headless JS can survive app termination with proper configuration.
**The background execution spectrum:**
```
Most reliable Least reliable
| |
Push notifications > Foreground services > Background tasks > Timers
(instant delivery) (visible to user) (OS-scheduled) (killed)
```
Background tasks sit in the middle -- more reliable than timers, but entirely at the OS's discretion. For critical work, combine with push notifications as a trigger.
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: Expo Background Task (expo-background-task)
The modern Expo approach using BGTaskScheduler (iOS) and WorkManager (Android). Replaces the older `expo-background-fetch`.
```typescript
import * as TaskManager from "expo-task-manager";
import * as BackgroundTask from "expo-background-task";
const SYNC_TASK_NAME = "BACKGROUND_SYNC_TASK";
const TWELVE_HOURS_IN_MINUTES = 720;
// MUST be top-level -- not inside a component
TaskManager.defineTask(SYNC_TASK_NAME, async () => {
try {
const hasNewData = await fetchLatestUpdates();
return hasNewData
? BackgroundTask.BackgroundTaskResult.Success
: BackgroundTask.BackgroundTaskResult.Failed;
} catch {
return BackgroundTask.BackgroundTaskResult.Failed;
}
});
```
**Why good:** Task defined at top-level scope (runs even when app launches in background), returns explicit result code, handles errors
```typescript
// BAD: Defining task inside a component
function App() {
useEffect(() => {
// This will NOT work when app starts from background
TaskManager.defineTask(SYNC_TASK_NAME, async () => {
/* ... */
});
}, []);
}
```
**Why bad:** Task definition inside component lifecycle will not execute when the OS launches the app headlessly in the background
See [examples/core.md](examples/core.md) for complete registration/unregistration lifecycle.
---
### Pattern 2: Bare RN Background Fetch (react-native-background-fetch)
For bare React Native projects (non-Expo). Wraps BGAppRefreshTask (iOS) and WorkManager (Android).
```typescript
import BackgroundFetch from "react-native-background-fetch";
const MIN_FETCH_INTERVAL_MINUTES = 15;
// Configure in app initialization (e.g., App component mount)
const status = await BackgroundFetch.configure(
{
minimumFetchInterval: MIN_FETCH_INTERVAL_MINUTES,
stopOnTerminate: false, // Android: continue after app killed
startOnBoot: true, // Android: restart after device reboot
enableHeadless: true, // Android: enable Headless JS
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY,
},
async (taskId) => {
// Task triggered -- do your work
await syncData();
BackgroundFetch.finish(taskId); // MUST call when done
},
async (taskId) => {
// Timeout -- OS is about to kill the task, clean up immediately
BackgroundFetch.finish(taskId);
},
);
```
**Why good:** Explicit timeout handler for graceful cleanup, `finish(taskId)` signals OS completion, Android-specific options for post-termination behavior
See [examples/core.md](examples/core.md) for scheduleTask one-shot/periodic tasks and Headless JS setup.
---
### Pattern 3: Background Location Tracking
Continuous location updates while backgrounded. Uses `expo-location` with `expo-task-manager`. Requires explicit background permission grants ("Always Allow" on iOS).
```typescript
import * as TaskManager from "expo-task-manager";
import * as Location from "expo-location";
const LOCATION_TASK_NAME = "BACKGROUND_LOCATION_TASK";
// Top-level task definition
TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
if (error) {
console.error("Location task error:", error.message);
return;
}
if (data) {
const { locations } = data as { locations: Location.LocationObject[] };
await saveLocationsToServer(locations);
}
});
```
**Why good:** Top-level definition, explicit error handling, typed location data extraction
See [examples/core.md](examples/core.md) for permission flow, start/stop, and accuracy configuration.
---
### Pattern 4: Android Headless JS
Android-only mechanism for running JavaScript after app termination. Requires native setup and registration in `index.js`.
```javascript
// index.js -- register headless task alongside app
import { AppRegistry } from "react-native";
import { App } from "./App";
const APP_NAME = "MyApp";
const HEADLESS_TASK_NAME = "com.transistorsoft.fetch"; // Default ID from react-native-background-fetch
AppRegistry.registerComponent(APP_NAME, () => App);
// Headless task runs when app is terminated but task fires
AppRegistry.registerHeadlessTask(HEADLESS_TASK_NAME, () => async (taskData) => {
await performSync(taskData);
// Task completes when promise resolves
});
```
**Why good:** Registered at app entry point, async function allows proper cleanup, runs even after app termination on Android
**Gotcha:** Headless JS is Android-only. iOS has no equivalent -- once the user force-quits the app, no background tasks run until the user reopens it.
See [examples/core.md](examples/core.md) for complete headless setup with `enableHeadless` configuration.
---
### Pattern 5: Task Registration and Unregistration Lifecycle
Always check registration status before registering, and unregister when tasks are no longer needed.
```typescript
async function ensureBackgroundSyncRegistered(): Promise<void> {
const isRegistered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK_NAME);
if (isRegistered) return;
await BackgroundTask.registerTaskAsync(SYNC_TASK_NAME, {
minimumInterval: TWELVE_HOURS_IN_MINUTES,
});
}
async function disableBackgroundSync(): Promise<void> {
const isRegistered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK_NAME);
if (!isRegistered) return;
await BackgroundTask.unregisterTaskAsync(SYNC_TASK_NAME);
}
```
**Why good:** Guards against double-registration, idempotent enable/disable, named constants for intervals
See [examples/core.md](examples/core.md) for status checking and debugging patterns.
</patterns>
---
<decision_framework>
## Decision Framework
### Choosing a Background Task Approach
```
What kind of background work do you need?
|
+-> Periodic data sync (every 15min - 12hrs)?
| +-> Expo project? --> expo-background-task
| +-> Bare RN? --> react-native-background-fetch
|
+-> Continuous location tracking?
| +-> Expo? --> expo-location + startLocationUpdatesAsync
| +-> Bare RN? --> react-native-background-geolocation
|
+-> Complete a task started in foreground (iOS 26+)?
| +-> BGContinuedProcessingTask (new in iOS 26)
|
+-> Long-running processing (ML, export)?
| +-> Foreground service with notification (Android)
| +-> BGProcessingTask (iOS, requires charger + network)
|
+-> Must survive app termination (Android)?
| +-> Headless JS + enableHeadless: true + stopOnTerminate: false
|
+-> Must execute at exact time?
+-> Not possible with background tasks
+-> Use push notifications + server-side scheduling
```
### expo-background-task vs expo-background-fetch
| Feature | expo-background-task | expo-background-fetch |
| ---------------- | ------------------------ | ----------------------- |
| Status | **Active** (recommended) | **Deprecated** |
| iOS API | BGTaskScheduler | Legacy Background Fetch |
| Android API | WorkManager | JobScheduler |
| Min interval | 15 minutes | ~10 minutes (advisory) |
| Network required | Yes (by default) | No |
| Reliability | Higher | Lower |
### Platform Execution Limits
| Constraint | iOS | Android |
| -------------------- | --------------------------------------------- | ---------------------------------- |
| Refresh task time | ~30 seconds | ~10 minutes |
| Processing task time | Several minutes (charger required) | ~10 minutes |
| Minimum interval | 15 minutes (system-managed) | 15 minutes (WorkManager-enforced) |
| After force-quit | No tasks run | Headless JS can run (with config) |
| After reboot | Tasks resume automatically | Requires `startOnBoot: true` |
| Simulator support | No (physical device only for BGTaskScheduler) | Partial (Doze may not be enforced) |
</decision_framework>
---
<red_flags>
## RED FLAGS
**High Priority Issues:**
- Defining tasks inside React components or useEffect -- tasks MUST be at the top-level scope or they won't run when the app starts from background
- Not calling `finish(taskId)` or returning a result -- the OS will penalize your app, reducing future scheduling frequency or killing the task
- Expecting exact timing -- background task intervals are minimums, the OS may delay execution by hours or even days on iOS
- Using `setTimeout`/`setInterval` for background work -- these are killed immediately when the app is backgrounded
- Not requesting background permissions -- iOS requires Info.plist UIBackgroundModes, Android requires ACCESS_BACKGROUND_LOCATION and RECEIVE_BOOT_COMPLETED
**Medium Priority Issues:**
- Doing heavy computation in a background refresh task -- iOS gives ~30 seconds, not minutes
- Not handling the timeout/expiration callback -- if the OS decides to stop your task early, you must save progress and exit
- Assuming background location works with "When In Use" permission -- it requires "Always Allow" on iOS
- Testing only on simulators -- iOS simulators do not execute BGTaskScheduler tasks
- Not checking `getStatusAsync()` before registering -- background tasks may be restricted by user settings or device state
**Gotchas & Edge Cases:**
- iOS force-quit kills ALL background tasks until user reopens the app -- there is no workaround
- Android vendor battery optimizations (Samsung, Xiaomi, Huawei) may kill background tasks beyond stock Android Doze restrictions -- see dontkillmyapp.com
- `expo-background-task` requires network connectivity by default -- tasks won't run offline
- iOS BGTaskScheduler uses machine learning to predict when to run your task -- it may take days to "settle in" for newly installed apps
- WorkManager enforces a hard 15-minute minimum interval -- you cannot schedule more frequently
- Headless JS is Android-only -- iOS has no equivalent post-termination execution
- `expo-background-fetch` is deprecated in favor of `expo-background-task` -- migrate to the new API
- Background tasks registered with expo-task-manager persist across app restarts -- always check `isTaskRegisteredAsync` before re-registering
- Android 15/16 edge-to-edge changes do not affect background tasks, but foreground service notification requirements have tightened
- iOS 26 introduces BGContinuedProcessingTask for completing user-initiated work in the background -- a new option for tasks started in foreground
</red_flags>
---
<critical_reminders>
## CRITICAL REMINDERS
> **All code must follow project conventions in CLAUDE.md**
**(You MUST define tasks in the top-level scope (global) -- tasks defined inside React components or lifecycle methods will NOT be registered when the app starts from the background)**
**(You MUST call `finish(taskId)` or return a `BackgroundTaskResult` when task execution completes -- failing to signal completion causes the OS to penalize or kill your app)**
**(You MUST request background permissions explicitly on both platforms -- iOS requires Info.plist UIBackgroundModes entries, Android requires manifest permissions)**
**(You MUST handle the OS killing your task at any time -- use expiration listeners on iOS and timeout callbacks on Android to clean up gracefully)**
**(You MUST keep background work minimal -- sync only changed data, avoid heavy computation, respect the ~30s iOS refresh limit)**
**Failure to follow these rules will result in tasks that never execute, apps penalized by the OS scheduler, or apps rejected from the App Store for excessive background resource usage.**
</critical_reminders>