expo-background-task and expo-task-manager for deferred uploads. Mobile OSes kill foreground network work when users switch apps. Register a background worker to flush SQLite outboxes, upload attachments, and pull deltas - within strict battery and scheduling limits.
// src/background/syncTask.ts - import this file at app entry (global scope)import * as BackgroundTask from "expo-background-task";import * as TaskManager from "expo-task-manager";export const SYNC_TASK = "offline-sync-worker";TaskManager.defineTask(SYNC_TASK, async () => { try { // Open DB, flush outbox, upload files - keep work short console.log("[sync] background worker ran"); return BackgroundTask.BackgroundTaskResult.Success; } catch (error) { console.error("[sync] background worker failed", error); return BackgroundTask.BackgroundTaskResult.Failed; }});
// src/background/registerSyncTask.tsimport * as BackgroundTask from "expo-background-task";import * as TaskManager from "expo-task-manager";import { SYNC_TASK } from "./syncTask";export async function registerSyncTask() { const status = await BackgroundTask.getStatusAsync(); if (status === BackgroundTask.BackgroundTaskStatus.Restricted) return; const registered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK); if (!registered) { await BackgroundTask.registerTaskAsync(SYNC_TASK, { minimumInterval: 15, // minutes - OS may delay further }); }}
// index.ts or app/_layout.tsx - side-effect import at topimport "@/background/syncTask";
When to reach for this:
Flushing pending SQLite / AsyncStorage outbox when app is backgrounded
Uploading inspection photos after field capture
Incremental catalog pull that should not block foreground UX
When to avoid:
User waiting for immediate confirmation - flush on reconnect in foreground
Sub-minute polling - OS will not honor it; use push notifications or foreground refetch
Expo prebuild / CNG applies these via the expo-background-task plugin. Bare projects must edit Info.plist manually. Simulators do not run BGTaskScheduler - test on device.
minimumInterval: 15 is the floor in minutes. WorkManager also expects network + battery constraints - your task runs when conditions align, not exactly at T+15.
defineTask inside a component - Task undefined on cold start from OS. Fix: Global module import at entry.
Expecting immediate execution - First run may be hours later on iOS. Fix: Foreground flush on reconnect; background for leftovers.
Testing only on iOS Simulator - BG tasks never fire. Fix: Physical device + triggerTaskWorkerForTestingAsync in __DEV__.
Long synchronous work in defineTask - iOS kills tasks that exceed budget. Fix: Batch uploads; listen for expiration; return Success early.
Using React hooks inside defineTask - No React tree in background entry. Fix: Open SQLite / AsyncStorage directly; no useQueryClient.
Forgetting prebuild after adding plugin - Console: No task request with identifier … scheduled. Fix:npx expo prebuild or EAS build with updated native config.
Multiple registered tasks - Expo uses a single native worker - last registration wins minimum interval. Fix: One defineTask orchestrating all deferrable work.
Requires a development build for full native behavior - verify on device after npx expo prebuild.
Why must defineTask be global?
When iOS/Android wakes your app for a background event, React may not be mounted. The native runtime looks up the task by name in TaskManager's global registry - registration inside a component runs too late or not at all.
What minimumInterval should I use?
Start with 15 (minutes) - the platform minimum. iOS may still defer to overnight windows. Do not use background tasks for sub-15-minute freshness requirements.
Design for resume on next open + outbox durability in SQLite.
Can I use TanStack Query in the background task?
Not directly - no provider in the background entry. Flush data in imperative code; invalidate Query cache next time the user opens the app foreground.
What return value should defineTask use?
Return BackgroundTask.BackgroundTaskResult.Success when work completed or nothing was pending. Return Failed when data is still pending and you want the OS to consider retry - pair with idempotent flush logic.
How does this relate to expo-task-manager location tasks?
Same TaskManager.defineTask API - different trigger sources. Background sync uses expo-background-task scheduling; location uses expo-location APIs. Keep task names unique per worker type.