Skip to content
Blog

Background Fetch in Flutter with the background_fetch Package

background_fetch 1.7.0 wakes your Flutter app every 15 minutes with BackgroundFetch.configure, scheduleTask, headless Android events, and JobScheduler constraints.

Published on • November 5, 2026

AI Assistant

Both mobile operating systems will wake your app while it is closed, but neither promises you a schedule — and every Flutter plugin in this space is a negotiation with the OS. background_fetch is the small, honest version of that negotiation: version 1.7.0 (published May 2026, MIT, verified publisher transistorsoft.com), Android and iOS only, roughly 18k downloads a week.

It does one thing: give your callback a short burst of background runtime on a throttled cadence, plus the ability to schedule extra one-shot or periodic jobs.

What the OS actually promises

The package README states the hard ceiling twice: you will never receive a fetch event faster than 15 minutes, and there is no way to raise the rate. minimumFetchInterval defaults to 15 and its minimum allowed value is 15 — it is a hint, not a schedule. iOS tunes timing from usage patterns and battery state, and can take days before its machine-learning model settles into a regular rhythm.

Platform caveats from the same README:

  • On iOS there is no such thing as stopOnTerminate: false — once the user terminates the app, iOS stops firing events.
  • iOS scheduleTask events appear to fire mainly while the device is plugged into power.
  • If a user stops opening your iOS app for long periods, events stop entirely.
  • Android ships a headless implementation, so events can still be handled after termination.

Check the authorization before you build anything around it:

final status = await BackgroundFetch.status;
// 0 STATUS_RESTRICTED, 1 STATUS_DENIED, 2 STATUS_AVAILABLE

configure() and the finish() contract

The entry point is BackgroundFetch.configure(config, onFetch, [onTimeout]), which starts the plugin immediately:

int status = await BackgroundFetch.configure(
  BackgroundFetchConfig(minimumFetchInterval: 15),
  (String taskId) async {
    try {
      await syncInbox();
    } finally {
      BackgroundFetch.finish(taskId); // required
    }
  },
  (String taskId) {
    BackgroundFetch.finish(taskId); // timeouts: finish immediately
  },
);

The callbacks receive only a taskId string; the timeout variant is the optional third argument. The one rule you cannot skip: always call BackgroundFetch.finish(taskId). The API reference warns that skipping it gets your app penalized — iOS may terminate it and cut future wake frequency, while Android treats the JobScheduler job as incomplete and throttles or blocks later runs. Call it even when the work throws.

Android: constraints, AlarmManager, and headless events

Android runs fetch events on JobScheduler, falling back to AlarmManager on older devices, and exposes scheduling constraints that iOS does not:

BackgroundFetch.configure(
  BackgroundFetchConfig(
    minimumFetchInterval: 15,
    stopOnTerminate: false,
    startOnBoot: true,
    enableHeadless: true,
    requiresBatteryNotLow: true,
    requiredNetworkType: BackgroundFetch.NETWORK_TYPE_UNMETERED,
  ),
  (String taskId) async {
    BackgroundFetch.finish(taskId);
  },
);

stopOnTerminate: false (default true) keeps events flowing after the user swipes the app away, and both startOnBoot and enableHeadless require it. requiredNetworkType accepts NETWORK_TYPE_NONE, ANY, CELLULAR, UNMETERED or NOT_ROAMING; the other switches are requiresCharging, requiresDeviceIdle, requiresStorageNotLow. forceAlarmManager: true bypasses JobScheduler entirely — the docs are explicit that it is less battery-efficient and should be a fallback, not a default.

Headless handling means a top-level function registered at startup:

@pragma('vm:entry-point')
void backgroundFetchHeadlessTask(HeadlessEvent task) async {
  if (task.timeout) {
    BackgroundFetch.finish(task.taskId);
    return;
  }
  await quickSync();
  BackgroundFetch.finish(task.taskId);
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
  runApp(const MyApp());
}

The @pragma('vm:entry-point') matters: tree-shaking can otherwise remove a function that is only referenced from native code.

Extra jobs with scheduleTask

BackgroundFetch.scheduleTask(TaskConfig(...)) adds jobs beyond the default fetch event. They reuse the same onFetch callback, identified by their taskId, and you cancel them with BackgroundFetch.stop(taskId) (with no argument, stop() cancels everything).

await BackgroundFetch.scheduleTask(TaskConfig(
  taskId: 'com.example.periodic-sync',
  delay: 600000, // milliseconds
  periodic: true,
  stopOnTerminate: false,
  requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY,
));

On iOS, custom tasks are registered through the BGProcessingTask API, so every taskId must be declared in Info.plist under BGTaskSchedulerPermittedIdentifiers before you schedule it. Android applies the same JobScheduler/AlarmManager constraint set as configure.

background_fetch or workmanager?

workmanager 0.10.10 (published September 2026, MIT, fluttercommunity.dev) is the heavier tool: a wrapper around Android’s WorkManager, iOS Background Tasks and macOS NSBackgroundActivityScheduler, with federated workmanager_android, workmanager_apple, and experimental workmanager_web/workmanager_linux packages. You register one-off or periodic tasks and handle them in a single Workmanager().executeTask((task, inputData) async => true) dispatcher, which is the natural home for database maintenance, file uploads and cleanup jobs that need real constraints and retries.

Choose background_fetch when the work is a light, frequent poll — refreshing a token, checking a feed, sending a heartbeat — and you want iOS background-refresh semantics with an Android headless path. Choose workmanager when a task must survive longer, run under network or charging constraints, or be guaranteed by the OS’s own job scheduler rather than opportunistic fetch windows. Battery-wise, both prefer you do little per wake: batch the request, respect finish(), and let requiresBatteryNotLow or Wi-Fi-only constraints defer non-urgent work.

Takeaway: Use background_fetch for short, throttled pings — configure() with minimumFetchInterval: 15, an unconditional finish(taskId), and headless registration on Android — and move genuinely heavy jobs to workmanager, which is built for constrained, retryable background work.