All posts
Platform Deep Dives8 min read

Background processing in Flutter. What a native developer needs to know.

Isolates, WorkManager on Android, BGTaskScheduler on iOS, and the places where Flutter's abstraction breaks down. Native Kotlin and Swift equivalents included.

The first time I tried to do "background work" in Flutter, I assumed it was like spawning a thread. It is not. Dart isolates do not share memory, the platform's background execution APIs are nothing like each other, and the most common Flutter packages for background work hide a lot of platform behaviour you eventually have to understand. The first user complaint about a notification that never fired taught me what those abstractions hide.

This post is the version a native iOS or Android engineer needs to read before designing a Flutter app that does anything in the background. I will cover what isolates actually are, how they relate to platform background execution, and where the abstractions stop being honest.

Context: three different things called "background"

When a Flutter developer says "background," they could mean any of three things:

  1. CPU-bound work that does not block the UI thread (an isolate).
  2. Work that runs while the app is in the foreground but not on the main thread (also usually an isolate).
  3. Work that runs while the app is suspended or terminated by the OS (WorkManager / BGTaskScheduler / push-triggered isolates).

These are different problems. The same package, workmanager, sometimes solves problem 3 but not 1 or 2. The same primitive, compute(), solves 1 and 2 but not 3. Pick the wrong one and your "background work" silently does not run.

Isolates, the actual concurrency primitive

Dart isolates are not threads in the OS sense. Each isolate has its own heap, its own event loop, and its own garbage collector. They communicate by sending messages over SendPorts. There is no shared mutable state, which is a discipline you either love or fight.

The constraint that catches every native developer: the function passed to compute must be a top-level function or a static method, because isolates do not share closure context. You cannot capture a local variable.

For longer-lived isolates with bidirectional messaging:

The Kotlin equivalent for foreground threading is kotlinx.coroutines:

The Swift equivalent is structured concurrency:

Notice that both native versions share memory. Dart does not. That changes how you design data flow. Send messages, do not share references.

Platform background execution: nothing like each other

Android's WorkManager is a job scheduler that handles deferrable, guaranteed work. It runs in your app's process even when the app is not visible. It has constraints (network, battery, charging) and retry policies.

iOS has two relevant systems: BGAppRefreshTask for short refresh windows and BGProcessingTask for longer maintenance work. iOS schedules these opportunistically based on app usage; you cannot guarantee when (or whether) they run.

These are not interchangeable. Android can guarantee execution within constraints. iOS guarantees almost nothing.

The Flutter package layer

Two packages dominate background work:

  • workmanager — wraps WorkManager on Android and BGTaskScheduler on iOS.
  • flutter_local_notifications for scheduled notifications (different concern, often confused).

What the package does not tell you, but you must know:

  • The callback runs in a fresh isolate. Any plugin you used during initialization in the main isolate must be re-initialized in the callback. This catches everyone the first time.
  • The @pragma('vm:entry-point') annotation is required for the AOT compiler to keep the function. Without it, the function is tree-shaken and the background task fails silently in release.
  • On iOS, "periodic" is a hint, not a guarantee. The system decides when to run the task based on app usage patterns. A user who opens your app rarely will see the task run rarely.
  • On iOS, the maximum runtime per BGAppRefreshTask is around 30 seconds. BGProcessingTask allows longer (typically up to a few minutes) but only when the device is on power and not in use.

Native Kotlin equivalent

Native Swift equivalent

The Swift API forces you to schedule the next task inside the handler, set an expiration handler, and call setTaskCompleted exactly once. The workmanager plugin handles these details for you on the Flutter side, but if you misuse it (forgetting to return from executeTask, for example), you can still leak.

Lifecycle, drawn

Caption: Android can guarantee periodic execution within constraints; iOS cannot. The Flutter package abstraction hides this difference, which is what bites teams who assume parity.

Where the Flutter abstraction breaks down

  • The "periodic" task on iOS may run hourly, daily, or never, depending on user behaviour. Do not depend on it for anything time-critical.
  • iOS limits how often background tasks can run for an app. Schedule frequency is advisory, not guaranteed.
  • Push notifications are the only reliable way to wake an iOS app for time-sensitive work. Use a silent push (content-available: 1) and handle it in a background isolate via firebase_messaging's onBackgroundMessage.
  • The background isolate cannot access the same plugin instances as the main isolate. Re-initialize what you need.
  • Hot reload does not work in background isolates. Test on full release builds.

What I would do differently

  • I would have read the WorkManager documentation and the BGTaskScheduler documentation before reading the workmanager plugin docs. The plugin makes more sense once you know what it is wrapping.
  • I would have used a silent push for anything time-critical on iOS from day one. Trying to make BGAppRefreshTask reliable was wasted effort.
  • I would have written a test that ran my background callback in isolation and checked it could initialize all its plugins. Plugin re-init failures were 80% of my early bugs.
  • I would have logged every background task run with a timestamp to a local file, so I could see in production whether tasks were actually executing. Without that, you are guessing.
  • I would not have used a periodic task to update widgets. iOS Widget timeline is the right primitive for that, not BGAppRefreshTask.

Closing opinion

For Android-only background work, workmanager plus WorkManager is genuinely good. For iOS background work, do not rely on BGTaskScheduler for anything important; use silent push notifications and a small background isolate. The Flutter abstraction is a thin wrapper, and the platform realities show through. For deeper platform integration generally, see Writing a Flutter platform channel in Swift. For where this fits with the iOS-feel discussion, see Flutter on iOS still does not feel native.

Written by the author of Flutterstacks

A developer who shipped production apps in Swift, Kotlin, and Dart — with a genuine native reference point that most Flutter writers simply don't have.

More articles →

Continue reading

You may also enjoy

Platform Deep Dives

Writing a Flutter platform channel in Swift. What the official docs skip.

A complete walk through a production platform channel in Swift, including threading, error propagation, and passing complex objects. The parts the docs gloss over.

Read article
Platform Deep Dives

Android back gesture handling in Flutter vs native. The edge cases nobody talks about.

Predictive back, PopScope, GoRouter, and the bottom sheet that swallows your gesture. The Android back gesture story for Flutter, with Kotlin equivalents.

Read article