Skip to content
Blog

Flutter Background Tasks with WorkManager

Run periodic and one-off background tasks in Flutter with the workmanager package — registration, the callbackDispatcher contract, platform setup, and the pitfalls that make tasks silently skip.

Published on • October 29, 2026

AI Assistant

Your app needs to sync data every morning, upload files after the user leaves, and clean stale cache overnight. None of that happens if the only entry point is a button tap — the OS freezes or kills your process when the app is backgrounded, and a Timer inside main() dies with it. Scheduling work with the OS is the answer, and in Flutter the standard wrapper for that is the workmanager package from fluttercommunity.

In this tutorial, you will learn how workmanager maps onto Android’s WorkManager, iOS’s BGTaskScheduler, and (experimentally) web and Linux, how to register one-off and periodic tasks, the hard constraints your background callback must respect, platform setup notes, and the common pitfalls — OEM battery optimization above all — that make tasks silently stop running.

What workmanager is

Per its pub.dev listing, workmanager lets you “execute Dart code in the background, even when your app is closed.” It’s a Flutter wrapper around Android’s WorkManager, iOS Background Tasks (BGTaskScheduler), and macOS NSBackgroundActivityScheduler — with an important caveat for Apple platforms: on macOS, tasks run while the app is running or backgrounded and the Mac is awake, not after quit.

Since version 0.10.x the plugin is federated:

  • workmanager — the unified Dart API you depend on
  • workmanager_android — Android implementation
  • workmanager_apple — iOS (BGTaskScheduler) and macOS implementation
  • workmanager_web — experimental, Service Worker + Web Worker execution
  • workmanager_linux — experimental, systemd user units

The web implementation is worth knowing about: it maps periodic tasks to Periodic Background Sync and runs the compiled Dart callback inside a Service Worker when the page is closed — with “honest limitations” (no exact scheduling, Chromium + installed PWA required). The Linux implementation uses systemd-run transient units for one-off tasks and .timer/.service pairs for periodic tasks with Persistent=true catch-up; tasks launch the app in headless --background-task mode, and constraints, backoff, and tags aren’t supported yet. Both are marked experimental — treat mobile as the primary target.

Install and register

Add the package:

flutter pub add workmanager

The core API, straight from the package’s quick example:

import 'package:flutter/material.dart';
import 'package:workmanager/workmanager.dart';

@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    debugPrint("Background task: $task");
    // Your background work here.
    return Future.value(true);
  });
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  Workmanager().initialize(
    callbackDispatcher,
    isInDebugMode: kDebugMode,
  );
  Workmanager().registerOneOffTask(
    "task-id",
    "simpleTask",
  );
  runApp(const MyApp());
}

Three things to note:

  • @pragma('vm:entry-point') on callbackDispatcher. In release builds with tree-shaking, the AOT compiler can strip code it can’t see is used. The pragma marks the function as an entry point so it survives into the release binary. Forgetting it is the #1 reason background tasks work in debug and do nothing in release.
  • Workmanager().initialize(callbackDispatcher, ...) must run before any registration, and must be called from your app’s initialization flow.
  • Two-part task identity: registerOneOffTask("task-id", "simpleTask") takes a unique name and a task name your dispatcher switches on. The unique name is what you’ll later use to cancel or replace the task.

Periodic tasks

For recurring work, use registerPeriodicTask instead — the same call shape, but the OS reschedules it on an interval:

await Workmanager().registerPeriodicTask(
  "sync-42",            // unique name — keep it stable
  "periodicDataSync",
  frequency: const Duration(hours: 12),
);

Android’s WorkManager imposes a minimum interval of 15 minutes; anything more frequent gets clamped by the OS, so design your task to be idempotent and to handle “ran earlier than expected” gracefully.

The background callback contract

Your task body runs in a stripped-down environment with no UI and a limited runtime. The constraints:

  • Return a bool (via Future). true means success; false tells the OS the task failed (which can trigger retry/backoff on Android).
  • No UI, no context. There’s no BuildContext, no snackbar, no navigation. If the user must see something, schedule a notification through platform code or persist data for the app to show on next launch.
  • Keep it short. Android’s background execution window is limited. For bulk uploads, downloads, or ML work, Android tasks can run as a foreground service with a persistent notification by passing a foregroundServiceConfig to registerOneOffTask or registerPeriodicTask:
await Workmanager().registerOneOffTask(
  "upload-task",
  "upload_files",
  foregroundServiceConfig: const ForegroundServiceConfig(
    notificationTitle: "Uploading files",
    notificationText: "Your files are being uploaded",
  ),
);
  • Don’t assume a warm engine. The dispatcher may run with the app process freshly started. Re-open databases, re-read preferences, and don’t rely on globals from your UI session.
  • Cancelling isn’t instant on the Dart side. cancelByUniqueName, cancelByTag, and cancelAll stop the WorkManager worker immediately, but the Dart callback that’s already running keeps executing unless it reacts. The package supports an onTaskStopped handler inside executeTask that receives the task name and a stop reason (cancelledByApp, timeout, preempt, and so on; Android < 12 reports unknown) — persist your state and return promptly. On iOS there’s no equivalent; the OS can’t stop a running task mid-flight from the app.

Platform setup notes

Android is the smoothest path: WorkManager handles scheduling, and the plugin wires the manifest entries for you. The main work is behavioral — see pitfalls below.

iOS goes through BGTaskScheduler, which means the OS decides when (and whether) your refresh happens, based on device conditions and user activity. BGTaskScheduler also requires your app to declare its task identifiers (the BGTaskSchedulerPermittedIdentifiers entries in Info.plist matching what you register) — the package’s Quick Start guide walks through the exact platform setup for each target. Expect less predictability than Android: “roughly every 12 hours” is a promise iOS doesn’t make.

macOS runs tasks via NSBackgroundActivityScheduler, but only while the app is running or backgrounded and the machine is awake — not after quit. Don’t design features that assume overnight runs on a closed lid.

Web and Linux exist as the experimental workmanager_web and workmanager_linux packages described above; both ship with explicit limitations you should read before committing to them.

Common pitfalls

  • OEM battery optimization (Android). Samsung, Xiaomi, Huawei, and others aggressively kill background work on devices where the user hasn’t exempted your app. The project’s troubleshooting guide exists precisely because “tasks stop after app close” is the most reported issue — the causes it lists are OEM battery optimization, constraints, and the app-verification policy on Android 12+. There’s no code fix: guide users to disable battery optimization for your app, keep tasks infrequent, and make sync resilient to being missed.
  • The stripped callback. Missing @pragma('vm:entry-point') means release builds silently skip the task. Always test on a release build, not just flutter run.
  • initialize ordering. Registering before initialize completes, or initializing lazily on a screen the user never opens, means nothing is scheduled.
  • Re-registering with the same unique name. On Android, registering again with the same unique name updates/replaces the existing task — keep names stable for “latest config wins,” or change them when you intend a separate parallel task.
  • Long-running work in a default task. Tasks killed by the OS timeout fail and retry. Use foregroundServiceConfig for genuinely long Android work.
  • Assuming iOS timing. BGTaskScheduler gives no scheduling guarantees. Design tasks to be idempotent and delta-based (“fetch what changed since last sync”) rather than time-critical.
  • Debugging blind. Start with isInDebugMode: true in initialize during development to see scheduling logs, and use the package’s debugging and troubleshooting guides when a task won’t fire.

Practical takeaways

  • workmanager wraps Android WorkManager, iOS/macOS background schedulers, and experimental web (Service Worker) and Linux (systemd) implementations behind one federated API.
  • Call Workmanager().initialize(callbackDispatcher) early, mark the dispatcher with @pragma('vm:entry-point'), and return true/false from executeTask to signal success.
  • Use registerOneOffTask for single jobs and registerPeriodicTask for recurring sync (15-minute minimum on Android); give both a stable unique name for cancellation.
  • Background callbacks have no UI, a limited runtime, and possibly a cold engine — write them as self-contained, idempotent units; use foregroundServiceConfig for long Android work.
  • On real devices, the biggest failure mode is OEM battery optimization and OS scheduling policy, not your Dart code — design for tasks being delayed or skipped, and test on release builds.

Conclusion & Next Steps

With workmanager, data sync, uploads, cleanup, and notification checks keep happening after your app leaves the foreground — one callback, one registration call, and the OS does the scheduling. Start with a single one-off task, verify it on a physical device in release mode, then add periodic sync with idempotent handlers. Next, explore constraints and backoff policies in the package’s customization docs, onTaskStopped for graceful cancellation, and platform-specific notification setup to surface background results to users.

Sources