skip to content

In an Expo app, why does a task registered with BackgroundTask.registerTaskAsync and minimumInterval: 15 not run every 15 minutes?

level: middleimportance: should knowfreq 38%

answer

  1. deferrable, not scheduled
  2. WorkManager on Android, BGTaskScheduler on iOS
  3. minimumInterval is a floor in minutes
  4. one shared worker, last interval wins
  5. network required, simulator and Expo Go excluded

basics

~20 s

expo-background-task hands the work to WorkManager on Android and BGTaskScheduler on iOS, which run it when battery, network and usage patterns suit them; minimumInterval is only a floor in minutes, so runs are best effort.

solid answer

~40 s

`expo-background-task` does not keep a timer. It asks the OS scheduler to run one worker: `WorkManager` with a connected-network constraint on Android, and a `BGProcessingTaskRequest` to `BGTaskScheduler` on iOS with an earliest begin date. `minimumInterval` is an inexact floor in minutes; the system decides the actual moment from battery, network and usage, and iOS often runs such tasks in windows like overnight. Every defined task shares that single worker, and the last registered task's interval decides its timing. Runs also stop after the user kills the app, and never happen on the iOS Simulator or in Expo Go. So treat it as best effort: persist data first, keep runs short, and repeat the work when the app returns to the foreground.

code

typescript · 20 lines
typescript
import * as BackgroundTask from 'expo-background-task';
import * as TaskManager from 'expo-task-manager';
import { flushQueuedLogs } from './logs';

const FLUSH_LOGS = 'flush-logs';

TaskManager.defineTask(FLUSH_LOGS, async () => {
  try {
    await flushQueuedLogs();
    return BackgroundTask.BackgroundTaskResult.Success;
  } catch {
    return BackgroundTask.BackgroundTaskResult.Failed;
  }
});

export async function scheduleLogFlush() {
  const status = await BackgroundTask.getStatusAsync();
  if (status === BackgroundTask.BackgroundTaskStatus.Restricted) return;
  await BackgroundTask.registerTaskAsync(FLUSH_LOGS, { minimumInterval: 15 });
}

go deeper

for a junior

Remember the key word, best effort: minimumInterval is a minimum in minutes, and the operating system decides when the task really runs.

for a middle

Explain the mechanism: WorkManager and BGTaskScheduler behind one shared worker, the network requirement, the 15-minute floor on Android, and the last-registered interval rule.

for a senior

Design so correctness never depends on a background run: persist first, keep runs short and idempotent, repeat the work on foreground, and test on devices with the testing trigger.

for a principal

Decide which features may rely on background work at all, and push anything time-critical to server push or scheduled notifications instead.

## What `expo-background-task` is `expo-background-task` runs **deferrable** background work in an Expo or bare React Native app. It does not run a timer of its own. It asks each operating system's scheduler to run one worker when the system judges it a good moment, and that worker calls your JavaScript tasks through `expo-task-manager`: - On **Android** it enqueues work with `WorkManager`, with a constraint that the network is connected. - On **iOS** it submits a `BGProcessingTaskRequest` to `BGTaskScheduler`, with network connectivity required and an earliest begin date of now plus the interval. It replaces `expo-background-fetch`, which the SDK 56 documentation marks deprecated and no longer patched. ## Why `minimumInterval: 15` does not mean every 15 minutes `BackgroundTask.registerTaskAsync(name, { minimumInterval })` takes an **inexact interval in minutes**. The source documents it as a minimum delay that the system treats as a floor, not a schedule: 1. **The OS picks the moment.** Both schedulers weigh battery level, charging, network availability and, on iOS, the user's usage patterns. The docs say iOS often ignores short intervals and tends to run tasks in windows such as overnight. 2. **Conditions must hold.** Without a network connection the worker is not run at all, and the docs add that low battery can hold it back. 3. **15 minutes is a floor on Android.** The documentation gives 15 minutes as the minimum interval for WorkManager, so a smaller value cannot make it run more often. 4. **One worker for all tasks.** Because both platforms limit how many tasks an app can schedule, the library runs every defined JavaScript task through a **single** worker, and the **last registered task's** interval decides when that worker runs. Registering a second task with a longer interval silently slows the first. 5. **The user can stop it.** When the user kills the app, tasks stop until it is launched again. On iOS, swiping the app away in the app switcher terminates it fully; on Android, removing it from recents does not always, and behaviour varies by device vendor. 6. **The environment can stop it.** The `BGTaskScheduler` API is unavailable on the iOS Simulator, and in Expo Go `getStatusAsync` reports the API as restricted; a development build on a real device is needed. So interviewers expect the phrase **best effort**: you are asking the platform for an opportunity, and the platform may say later, or not today. ## iOS details that bite - The app needs `processing` in `UIBackgroundModes` and the identifier `com.expo.modules.backgroundtask.processing` in `BGTaskSchedulerPermittedIdentifiers` in Info.plist. With Continuous Native Generation, prebuild adds both. - A run can be cut short. `BackgroundTask.addExpirationListener` (iOS) is called when the system stops the background executor, so save progress there; the library reschedules the worker automatically. ## Reading the status and the result `BackgroundTask.getStatusAsync()` returns a `BackgroundTaskStatus`: `Available` on a device where the API works, `Restricted` where it cannot run, such as the iOS Simulator or Expo Go. Check it before offering a setting that depends on background work, and explain to the user why it is unavailable. The executor you pass to `TaskManager.defineTask` returns a `BackgroundTaskResult`, `Success` or `Failed`, which the native side receives when the task completes. Returning `Failed` does not make the OS run the task sooner; it records the outcome. If a run fails, the data it was meant to process must still be where the next run, or the next foreground session, will find it. ## Designing around it | Need | Fit for `expo-background-task` | |---|---| | Refresh a cache or prefetch content "some time today" | Good | | Upload logs or analytics queued earlier | Good, with the data persisted first | | Deliver a reminder at 9:00 exactly | No; use a scheduled local notification | | Keep a live connection or track continuously | No; that needs a specific background mode such as location, or a server push | Practical rules: - Keep each run short and idempotent, and persist results before returning `BackgroundTaskResult.Success`. - Do the same work when the app comes to the foreground, so correctness never depends on a background run happening. - Test with `BackgroundTask.triggerTaskWorkerForTestingAsync()`, which runs registered tasks on demand in debug builds only. - Register once; `registerTaskAsync` returns early if the task is already registered.

  • An Expo app registers two background tasks, one with minimumInterval 15 and one with 240; how often does the first one run?
    `expo-background-task` runs all defined JavaScript tasks through a single platform worker, and the last registered task's interval sets that worker's timing. If the 240-minute task was registered last, both tasks run no more often than every four hours, and still only when the OS allows. Register tasks in a deliberate order, or accept one shared cadence.
  • How do you test an expo-background-task task without waiting for the OS scheduler?
    Call `BackgroundTask.triggerTaskWorkerForTestingAsync()`, which runs registered tasks directly on Android and invokes `BGTaskScheduler` on iOS. It only works in debug builds and returns `false` in production. On iOS use a physical device, because the Background Tasks API is unavailable on the Simulator.

Registering a background task is like leaving your name with a busy maitre d' who says 'no sooner than 15 minutes': you will be seated when a table, the staff and the kitchen all line up, which might be an hour later or after closing.

saying these in an interview costs you the question

  • minimumInterval: 15 guarantees the task runs every 15 minutes.
  • Each registered task gets its own independently scheduled worker.
  • A background task keeps running after the user swipes the app away on iOS.
  • Background tasks can be tested on the iOS Simulator like any other code.
  • expo-background-fetch is the current recommended API for periodic work.