Choosing WorkManager or an Alternative for Android Background Work
Decide between coroutines, foreground services, AlarmManager, and WorkManager for Android background work, with a constrained upload example and concrete validation steps.
19 Jul 2026, 02:18 UTC

The decision: how should this background task run?
Most Android apps eventually need to run work off the main thread: uploading logs, syncing data, processing images. The mistake is reaching for the same tool every time. The useful rule is this: if the work must happen now and the user is waiting, use coroutines or a foreground service; if the work can wait and must eventually complete even if the app closes or the device reboots, use WorkManager. This guide compares the supported options, then shows a concrete WorkManager implementation and how to verify it actually ran.
Assumptions below: AndroidX WorkManager 2.9.x, Kotlin, minSdk anywhere from API 14 up (WorkManager handles the underlying scheduler differences for you).
Comparing the supported options
| Option | Survives app death / reboot | Constraint-aware | Best for | Key limit |
|---|---|---|---|---|
| Kotlin coroutines (app scope) | No | No | Short work tied to the current session, e.g. parsing a response | Cancelled when the process dies |
| Foreground service | While running (with notification) | No | User-visible, immediate long work, e.g. music playback, active upload | Requires a persistent notification; stricter rules on Android 12+ |
| WorkManager (one-time / periodic) | Yes, persisted across reboots | Yes (network, charging, idle, storage) | Deferrable, guaranteed work, e.g. log upload, periodic sync | ~10 minute execution window per worker; not for exact timing |
| AlarmManager (exact) | Yes | No | Exact-time events like reminders | Needs SCHEDULE_EXACT_ALARM permission on Android 12+; no retry/backoff |
Trade-offs that matter in practice
Guarantee vs. immediacy. WorkManager persists your request in a database and re-runs it after process death or reboot, but the system decides when it runs based on constraints and battery policy. If you enqueue a task and expect it to fire within seconds, you will be disappointed on devices with aggressive power management.
Constraints are the main reason to choose it. Declaring NetworkType.UNMETERED plus setRequiresCharging(true) means a heavy sync only runs on Wi-Fi while plugged in, with zero scheduling logic on your side. On API 23+ WorkManager delegates to JobScheduler; on older devices it falls back to AlarmManager and a BroadcastReceiver. You get one API across all of it.
The 10-minute window. Each doWork() call gets roughly 10 minutes before the system stops it. Chunk large jobs (e.g. upload in batches, persist progress, return Result.retry()) or use setForeground() inside a Worker for expedited long work with a notification.
Periodic work is coarse. The minimum periodic interval is 15 minutes, and the flex window means execution is not exact. If you need a precise alarm, that is AlarmManager's job, not WorkManager's.
Concrete implementation: constrained log upload
Add the dependency in your app module's build.gradle (requires a project sync; no special permissions needed for the library itself):
implementation "androidx.work:work-runtime-ktx:2.9.0"
Define the worker. doWork() runs on a background thread provided by the library:
class LogUploadWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
val file = File(applicationContext.filesDir, "pending.log")
if (!file.exists()) return Result.success() // nothing to do
return try {
upload(file) // your network call; keep it under the time window
Result.success()
} catch (e: IOException) {
if (runAttemptCount < 5) Result.retry() else Result.failure()
}
}
}
Enqueue it with constraints, from anywhere with a Context (e.g. your Application class or a ViewModel):
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true)
.build()
val request = OneTimeWorkRequestBuilder<LogUploadWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"log-upload",
ExistingWorkPolicy.KEEP, // don't duplicate if already queued
request
)
enqueueUniqueWork with KEEP is the practical choice for tasks like this: tapping "sync" twice won't schedule two uploads.
Validating that the work actually ran
Don't assume success from enqueueing — enqueueing only persists the request. Verify execution:
- Observe state in code. Use
WorkManager.getInstance(context).getWorkInfoByIdLiveData(request.id)and check theState:ENQUEUED→RUNNING→SUCCEEDED. If it staysENQUEUED, your constraints aren't met (e.g. the emulator is on a metered connection). - Inspect via adb. Run
adb shell dumpsys jobscheduler | grep your.packageon an API 23+ device or emulator to confirm the system scheduled the backing job and see which constraints are unsatisfied. No root needed. - Force-test constraints. Toggle airplane mode or switch the emulator to cellular in Settings to watch the work move from
ENQUEUEDtoRUNNINGonly when conditions match. - Check Logcat. Filter on your app's tag; add logging at the top of
doWork()and before eachResultreturn so retries are visible.
A passing check is: the WorkInfo reaches SUCCEEDED, and re-running dumpsys jobscheduler shows no pending job for your package (one-time work is cleaned up after completion).
Limitations to keep in mind
- WorkManager is not for exact timing; expect delays from Doze and OEM battery restrictions, which vary by manufacturer.
- Force-stopping an app (Settings → Apps → Force stop) cancels scheduled work until the user reopens the app — this is platform behavior, not a bug in your code.
- If you initialize WorkManager with a custom
ConfigurationviaWorkManagerInitializerremoval in the manifest, test cold starts carefully; a misconfigured provider is a common crash source. - Version note: examples target work-runtime-ktx 2.9.0. Check the current stable release before copying the dependency line into a production build.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.