workmanager_web: Background Dart Tasks via Service Worker (Experimental)
workmanager now runs background tasks on the web through a Service Worker and Web Worker — with honest, documented limitations.
Published on • October 19, 2026
AI Assistant

“Execute Dart code in the background, even when your app is closed” is the pitch of Flutter’s workmanager plugin on mobile. As of version 0.10.x, the same API reaches the browser through workmanager_web (0.2.0, published by fluttercommunity.dev) — an experimental federated implementation that approximates background execution using a Service Worker for page-closed tasks and a Web Worker for real parallel execution while the page is open. The maintainers are unusually upfront that the web contract is a best-effort approximation, and the package’s README deserves credit for an “Honest limitations” section that most plugins skip.
Here’s how it works, how to set it up, and where the browser draws the line.
The federated family
workmanager is now a federated plugin, and the app-facing package delegates per platform:
- workmanager — the unified API.
- workmanager_android — Android WorkManager.
- workmanager_apple — iOS
BGTaskSchedulerand macOSNSBackgroundActivityScheduler. - workmanager_web — Service Worker + Web Worker execution (experimental).
- workmanager_linux — systemd user units (experimental;
systemd-runtransient units for one-off tasks,.timer/.servicepairs for periodic tasks withPersistent=truecatch-up). - workmanager_platform_interface — the contract between them.
Because workmanager depends on workmanager_web directly, Workmanager() works on web out of the box. For web-specific options you use WorkmanagerWeb directly.
How execution maps to the browser
The mechanics differ by situation, and the README documents them as a table:
| Situation | Mechanism |
|---|---|
| Page open | Tasks run in a dedicated Web Worker containing the compiled callback dispatcher (dart2js, Flutter-free) — off the main thread. Falls back to in-page execution if the bundle is missing. |
| Page closed — Periodic Background Sync | Chromium wakes the Service Worker roughly every frequency — but the browser minimum is 12 hours — for installed, engaged PWAs. The Service Worker then runs the compiled Dart dispatcher via importScripts and records the result in IndexedDB. |
| Page closed — Web Push | A push message wakes the Service Worker into the same Dart execution path. Requires a push server. |
| Page closed — fetch interception | Opportunistic: a same-origin request can wake the Service Worker to run due tasks. Unreliable — browsers throttle wake-ups. |
Results recorded while the page was closed are replayed into the app’s event log on the next load, so you can still observe them. Note the correction to the naive expectation: requesting registerPeriodicTask with a 15-minute frequency does not get you 15-minute background execution after the tab closes — Periodic Background Sync runs at roughly max(frequency, 12h), and only for installed, engaged PWAs in Chromium.
Setup: three moving parts
1. Dependency. Add the package (or just workmanager, which pulls it in):
dependencies:
workmanager_web: ^0.2.0
2. The Service Worker script. It’s plain JavaScript, living in the package’s web/ folder. Copy it into your app’s web/ so it’s served at /workmanager_service_worker.js:
cp ../workmanager_web/web/workmanager_service_worker.js web/
3. A Flutter-free dispatcher, compiled separately. A Service Worker cannot boot the Flutter engine, so your callback must live in a Dart file with no package:flutter/... imports, compiled with plain dart2js:
// lib/background_tasks.dart — pure Dart, no Flutter imports
import 'package:workmanager_web/execution.dart';
@pragma('vm:entry-point')
void callbackDispatcher() {
WorkmanagerExecution.instance.executeTask((taskName, inputData) async {
// Flutter-free background work only.
return true;
});
}
// web/background.dart — standalone dart2js entrypoint
import 'package:workmanager_web/worker.dart';
import 'package:my_app/background_tasks.dart';
void main() {
WorkmanagerWebWorker.run(callbackDispatcher);
}
dart compile js --no-source-maps -O2 web/background.dart -o web/background.dart.js
Commit the compiled .js next to your app’s web/ directory, then initialize and register:
import 'package:workmanager_web/workmanager_web.dart';
await WorkmanagerWeb().initialize(
callbackDispatcher,
dispatcherUrl: WorkmanagerWeb.defaultDispatcherUrl, // /background.dart.js
);
await WorkmanagerWeb().registerPeriodicTask(
'uniqueName',
'taskName',
frequency: const Duration(minutes: 15),
);
Talking to the worker
Task execution is request/response, but the package also supports free-form two-way messaging over postMessage for progress updates and “worker, do X now” flows:
// Page side
WorkmanagerWeb().workerMessages.listen((payload) {
print('worker says: $payload');
});
WorkmanagerWeb().sendMessageToWorker({'op': 'watch', 'city': 'cardiff'});
// Dispatcher side (Flutter-free bundle)
WorkmanagerExecution.instance.messageHandler = (payload) {
WorkmanagerExecution.instance.sendToPage?.call({'kind': 'ack'});
};
With the page open, messages travel to the dedicated Web Worker; without one, they’re delivered in-page. When execution happens inside the Service Worker with no page open, sendToPage output is dropped — persistent results still arrive via backgroundEvents on the next load. The WorkmanagerWeb API mirrors the main package: registerOneOffTask, registerPeriodicTask, cancelByUniqueName, cancelByTag, cancelAll, triggerTask, plus isScheduledByUniqueName and printScheduledTasks. iOS-only task types (registerProcessingTask, registerHealthResearchTask, registerContinuedProcessingTask) throw UnsupportedError on web.
The honest limitations, condensed
Straight from the package README:
- No exact scheduling. There is no web API for “run this at 15:00.” Periodic tasks map to Periodic Background Sync (
max(frequency, 12h), installed + engaged PWAs only). One-off tasks run on a page timer while open, and best-effort on the next Service Worker wake after their deadline when closed. - Chromium only for Periodic Background Sync — Safari and Firefox don’t implement it. In-page Web Worker execution works everywhere.
- Secure context required: HTTPS or
localhost. - The dispatcher must be Flutter-free. No plugins, no UI code — only what’s reachable from your pure-Dart dispatcher file gets compiled into the bundle.
- No notification on missed work. If the browser never wakes the Service Worker, the task is recorded as missed on next open.
- Experimental: API and file layout may change; don’t depend on it for production workloads yet.
Verify it before you trust it
The maintainers published their own test procedure: run over HTTPS or localhost, install the PWA and interact with it (Periodic Background Sync only runs for installed, engaged PWAs), register a periodic task, then in DevTools → Application → Periodic Background Sync press periodicsync for your task’s tag. That DevTools trigger bypasses the 12-hour scheduler minimum, so you can verify the whole page-closed path — Service Worker executes the compiled Dart dispatcher, result lands in IndexedDB, event log replays it on reopen — in a minute instead of half a day.