Skip to content

WorkManager

Developer UtilitiesScheduler/Job ManagementKotlin

What it is

WorkManager schedules deferrable, guaranteed background work on Android, surviving process death and reboots while respecting battery restrictions.

Define a Worker, wrap it in a WorkRequest with constraints, and enqueue it. WorkManager persists the request and runs it when the constraints are satisfied.

Installation

implementation("androidx.work:work-runtime-ktx:2.9.1")

Getting started

The smallest useful thing you can do with it, and what each part means.

A constrained, retrying worker
class SyncWorker(ctx: Context, params: WorkerParameters) :
    CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result = try {
        val since = inputData.getLong("since", 0)
        repository.sync(since)
        Result.success()
    } catch (e: IOException) {
        // Retry with the configured backoff — transient failure.
        if (runAttemptCount < 3) Result.retry() else Result.failure()
    }
}

val request = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.UNMETERED)
            .setRequiresBatteryNotLow(true)
            .build()
    )
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .setInputData(workDataOf("since" to lastSync))
    .build()

WorkManager.getInstance(context)
    .enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, request)
enqueueUniqueWork with KEEP prevents duplicate sync jobs piling up when the user opens the app repeatedly — a very common bug with naive enqueueing.

Advanced usage

Where the library earns its place over a simpler alternative.

Chains and observing progress
WorkManager.getInstance(context)
    .beginWith(listOf(compressRequest, uploadThumbRequest))   // parallel
    .then(uploadRequest)                                      // then this
    .then(notifyRequest)
    .enqueue()

// Report progress from the worker.
setProgress(workDataOf("progress" to percent))

// Observe it.
WorkManager.getInstance(context)
    .getWorkInfosForUniqueWorkFlow("sync")
    .collect { infos ->
        val info = infos.firstOrNull() ?: return@collect
        when (info.state) {
            WorkInfo.State.RUNNING   -> showProgress(info.progress.getInt("progress", 0))
            WorkInfo.State.SUCCEEDED -> showDone()
            WorkInfo.State.FAILED    -> showError()
            else -> Unit
        }
    }
Input and output Data is capped at roughly 10 KB. Passing a file path or a database id rather than the content itself is not optional — larger payloads throw.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Work never runs
Constraints are unmet, or the device is in Doze. Check with getWorkInfos, and confirm the constraints are actually satisfiable.
IllegalStateException: Data cannot occupy more than 10240 bytes
Too much data passed in or out. Store the payload and pass a reference.

Best practices

  • Use enqueueUniqueWork to prevent duplicate jobs accumulating.
  • Pass identifiers in Data, never payloads — there is a ~10 KB limit.
  • Return Result.retry() only for transient failures, and cap attempts with runAttemptCount.
  • Use WorkManager for deferrable work; it is the wrong tool for anything needing to run immediately.

Background

Why it exists, and what it was reacting to.

Android's background execution limits made older approaches unreliable. WorkManager is the single recommended API: it picks the right underlying mechanism per OS version and persists work so it eventually runs.