WorkManager, Background Tasks, Foreground Services & Alarm Limitations
Reliable background execution in the age of battery optimization
Open interactive version (quiz + challenge)Real-world analogy
What is it?
WorkManager is Jetpack's library for deferrable, guaranteed background work. It wraps JobScheduler, AlarmManager, and BroadcastReceiver into a single API that survives app kills and device restarts. Foreground Services handle user-visible ongoing tasks. AlarmManager handles exact-time alarms but faces increasing OS restrictions from Android 12 onward.
Real-world relevance
In BRAC's field ops app, WorkManager handled offline data sync: a PeriodicWorkRequest every 15 minutes with CONNECTED constraint, chained with an upload task that had exponential backoff. When FCM delivered a data message (new assignment), it enqueued a OneTimeWorkRequest immediately. The foreground service ran during large file uploads so the system wouldn't kill the transfer mid-stream.
Key points
- WorkManager guaranteed execution — WorkManager guarantees task execution even if the app is killed or the device restarts. It uses JobScheduler (API 23+), AlarmManager+BroadcastReceiver (API <23), and BatteryOptimization-aware scheduling — you don't choose the backend, WorkManager does.
- OneTimeWorkRequest — For tasks that run once: val request = OneTimeWorkRequestBuilder().setConstraints(constraints).setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS).build(). The exponential backoff with jitter prevents thundering herd on server recovery.
- PeriodicWorkRequest — For recurring tasks with a minimum interval of 15 minutes: PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).setConstraints(constraints).build(). The system may batch and delay periodic work — do not rely on exact timing.
- Constraints — Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).setRequiresCharging(false).setRequiresBatteryNotLow(true).build(). In BRAC field ops, sync work required CONNECTED only — not UNMETERED — because field officers use mobile data.
- Chaining work — WorkManager.getInstance(context).beginWith(uploadRequest).then(notifyRequest).enqueue(). For parallel branches: beginWith(listOf(req1, req2)).then(mergeRequest). Chaining is DAG-based — downstream tasks receive output data from upstream via workDataOf().
- Worker vs CoroutineWorker — Worker runs on a background thread (Executor). CoroutineWorker runs on Dispatchers.IO and supports suspend functions — strongly preferred for Kotlin codebases. Override doWork() in both; return Result.success(), Result.failure(), or Result.retry().
- Foreground services requirement — Since Android 8 (API 26), long-running background services MUST call startForeground(id, notification) within 5 seconds of starting or the system kills the service with ANR-like behavior. The notification must remain visible for the service lifetime.
- Foreground service types (Android 14+) — Android 14 requires declaring foreground service type in the manifest: android:foregroundServiceType="dataSync|mediaPlayback|location". The wrong or missing type causes a SecurityException at startForeground() call.
- Doze mode impact — In Doze mode (screen off, stationary, unplugged), the system defers network access, wafers, and syncs to maintenance windows. WorkManager work with network constraints is automatically deferred. High-priority FCM messages can wake the device from Doze.
- AlarmManager exact alarms (Android 12+) — Android 12 (API 31) requires SCHEDULE_EXACT_ALARM permission for setExact() and setExactAndAllowWhileIdle(). Android 13 made this permission not pre-granted — users must grant it in Special app access settings. Always check canScheduleExactAlarms() before scheduling.
- WorkManager vs AlarmManager — Use WorkManager for deferrable, constraint-based, guaranteed background work (sync, upload, cleanup). Use AlarmManager only when exact timing is a hard user requirement (calendar reminders, medication alarms) — not for data sync or analytics flush.
- Battery optimization exemption — Requesting battery optimization exemption (REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) triggers a Play Store policy review. Only request it for apps with a genuine always-on need (health monitoring, IoT). WorkManager handles most cases without it.
Code example
// 1. CoroutineWorker — preferred for Kotlin
class SyncWorker(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val repo = ServiceLocator.getRepository(applicationContext)
repo.syncFromServer()
Result.success()
} catch (e: IOException) {
if (runAttemptCount < 3) Result.retry()
else Result.failure(workDataOf("error" to e.message))
}
}
}
// 2. Enqueue with constraints and chaining
fun scheduleSyncWork(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val periodicSync = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.addTag("sync")
.build()
// KEEP_EXISTING prevents duplicate periodic work
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"periodic_sync",
ExistingPeriodicWorkPolicy.KEEP,
periodicSync
)
}
// 3. Foreground service (Android 8+ requirement)
class UploadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Uploading data...")
.setSmallIcon(R.drawable.ic_upload)
.setOngoing(true)
.build()
// Must call within 5 seconds — or system kills the service
startForeground(NOTIF_ID, notification)
CoroutineScope(Dispatchers.IO).launch {
runUploadPipeline()
stopSelf()
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent?) = null
}
// 4. AlarmManager exact alarm (Android 12+)
fun scheduleExactAlarm(context: Context, triggerAtMillis: Long) {
val alarmManager = context.getSystemService(AlarmManager::class.java)
if (alarmManager.canScheduleExactAlarms()) {
val intent = PendingIntent.getBroadcast(
context, 0,
Intent(context, AlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP, triggerAtMillis, intent
)
} else {
// Request permission — launches system settings
val permIntent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
context.startActivity(permIntent)
}
}Line-by-line walkthrough
- 1. CoroutineWorker suspends on Dispatchers.IO automatically — no need to manage threads manually compared to the base Worker class.
- 2. runAttemptCount tracks how many times WorkManager has retried this worker instance — use it to limit retries before declaring failure.
- 3. Result.retry() signals WorkManager to reschedule with the configured backoff policy (EXPONENTIAL doubles the delay each attempt).
- 4. PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) sets the repeat interval — the system may delay beyond 15 min for battery optimization.
- 5. enqueueUniquePeriodicWork with KEEP policy ensures only one periodic sync worker runs at a time — calling it again does nothing if one already exists.
- 6. startForeground(NOTIF_ID, notification) must be called within 5 seconds of onStartCommand() to keep the service alive on Android 8+.
- 7. START_NOT_STICKY means if the system kills the service, it won't be restarted unless there are pending intents — correct for one-shot upload services.
- 8. stopSelf() in the coroutine's completion block ensures the service stops after the upload finishes, removing the persistent notification.
- 9. alarmManager.canScheduleExactAlarms() is required before calling setExact() on Android 12+ to avoid SecurityException.
- 10. PendingIntent.FLAG_IMMUTABLE is required for PendingIntents on Android 12+ as a security requirement.
- 11. setExactAndAllowWhileIdle() fires the alarm even during Doze mode — use only for user-facing time-critical alarms.
- 12. WorkManager.enqueue() with addTag() allows you to cancel or observe a group of related tasks by tag using cancelAllWorkByTag().
Spot the bug
class DataSyncWorker(ctx: Context, params: WorkerParameters) :
CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
return try {
syncData()
Result.success()
} catch (e: Exception) {
Result.retry() // Bug 1
}
}
}
class BackgroundSyncService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
CoroutineScope(Dispatchers.IO).launch {
longRunningSync() // Bug 2
stopSelf()
}
return START_STICKY
}
override fun onBind(intent: Intent?) = null
}
fun scheduleSync(context: Context) {
val work = PeriodicWorkRequestBuilder<DataSyncWorker>(
5, TimeUnit.MINUTES // Bug 3
).build()
WorkManager.getInstance(context).enqueue(work) // Bug 4
}
fun scheduleReminder(context: Context, timeMs: Long) {
val alarmManager = context.getSystemService(AlarmManager::class.java)
val pi = PendingIntent.getBroadcast(context, 0,
Intent(context, ReminderReceiver::class.java), 0) // Bug 5
alarmManager.setExact(AlarmManager.RTC_WAKEUP, timeMs, pi)
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- WorkManager Guide — Android Developers (Android Docs)
- Foreground Services Guide (Android Docs)
- Exact Alarms — Android 12 Changes (Android Docs)
- Battery Optimization Overview (Android Docs)
- WorkManager Advanced Topics — Chaining & Unique Work (Android Docs)