RizTech Academy logo
RizTech Academy
Work That Outlives the ScreenLesson 1 of 430 min

Why Android keeps killing your background work

Module 9 left Mandi Bhav with one honest gap: it only refreshes while somebody is looking at it. Priya grows onion near Lasalgaon, and what she wants is simple to say. At seven every evening, after the markets have closed, the phone in her pocket should fetch the day's prices, so that at six the next morning, before she loads the tractor, she can see where onion closed highest without waiting for one bar of signal to load anything. This module builds that. First, though, you need to know what you are up against, because Android spends a great deal of effort stopping exactly this kind of work, for good reasons, and every shortcut you might try has already been closed.

The phone is not yours

A mid-range phone in a village has a 5,000 mAh battery that has to last a day of calls, WhatsApp and a torch, often with no charger until evening. It may have forty apps installed, and every one of them would like to wake up "for a moment" to check something. If Android let them, the battery would be flat by lunch. So the rule since Android 6 has been: an app that is not on the screen runs only when the system decides it may, and the system is stingy.

You can see the cost of ignoring this from the other side too. The Play Console's Android vitals reports apps that keep the phone awake in the background, and the battery screen in Settings names the app to blame, which is usually the app that gets uninstalled.

What happens when the farmer swipes the app away

Start with what module 9 built. Its refresh runs in an application-wide scope, so it survives the screen that started it going away. A scratch run, on the course emulator (Android 16) with the local test server told to wait eight seconds before answering: open Mandi Bhav, tap "Refresh", and two seconds later swipe the app out of Recents.

00:38:29.674 I/OkHttp        --> GET http://10.0.2.2:8777/api/v1/prices.json
00:38:32.759 I/ActivityTaskManager: Destroy timeout of remove-task, attempt to kill Task{13b03e7 #462 type=standard A=10229:com.riztechacademy.mandibhav}
00:38:42.811 D/ActivityManager: freezing 7891 com.riztechacademy.mandibhav

At 00:38:37 the server tried to send the prices, and its write failed with a broken pipe: nothing on the phone was reading any more. Ten seconds after the swipe, Android froze the process outright. The prices were not saved. They arrived only because the experiment then reopened the app, 13 seconds after the swipe, which unfroze it; OkHttp reported the first attempt as failed after 15,315 ms, and module 9's retry fetched the file a second later. Had Priya put the phone in her pocket, nothing would have happened until she next opened the app.

In another run, with the swipe made while the process was doing background work, Android did not bother freezing it:

I/ActivityManager: Killing 31287:com.riztechacademy.mandibhav/u0a229 (adj 250): remove task

Killed in one run, frozen in the other, and in neither did the download finish. An application scope lives exactly as long as the process, and once your app is not on the screen, the process lives only as long as Android allows.

Five things that stop background work

Each of these was added in a different Android version, for a different reason, and they stack.

Mechanism Since What it does to you
Process death Always When memory is short, or the app is swiped away, the process goes. Coroutines, threads and timers go with it
Background execution limits Android 8 (API 26) An app in the background may not start an ordinary service
Doze Android 6 (API 23) Screen off, unplugged, still: network is cut and jobs, syncs and ordinary alarms wait for a maintenance window
App Standby Buckets Android 9 (API 28) Apps the user rarely opens get less job time and, in the lower buckets, no network for jobs
Force-stop Always Settings → Force stop: every job and alarm is cancelled, and nothing runs until the user opens the app

Some manufacturers add their own battery managers on top, which can be harsher still. You cannot test those on the emulator; you can only make sure your app does the right thing on stock Android, and tell users where the setting is if your app genuinely needs it.

Background services

Before Android 8, the usual way to work in the background was a Service. A scratch build tries it, starting a plain service from a broadcast receiver while the app is in the background:

I/M10Fgs: startService threw android.app.BackgroundServiceStartNotAllowedException: Not allowed to start service Intent { … PlainService }: app is in background uid UidRecord{436f542 u0a239 RCVR bg:+291ms idle …}

RCVR is the app's state: running a receiver, in the background. Older tutorials, and plenty of code you will inherit, still start services like this. On a phone running Android 8 or later, it throws.

Doze

You can force the emulator into Doze, which is the only way to test it without leaving a phone on a desk for an hour. The experiment: ask WorkManager (next lesson) for a refresh due in 60 seconds, close the app, and then:

adb shell dumpsys deviceidle force-idle

dumpsys jobscheduler describes the job while the phone dozes:

Unsatisfied constraints: CONNECTIVITY [0x10000000]
Ready: false

The refresh became due at 00:29:36. For the next one and a half minutes it waited, with its one unmet need being a network, because Doze had cut the network for background work. Nothing reached the server. Then:

adb shell dumpsys deviceidle unforce

at 00:31:15.3, and the worker logged "Daily refresh, attempt 1" at 00:31:18.7, three seconds later. On a real phone the "unforce" is a maintenance window, or Priya picking the phone up. Doze does not cancel your work; it delays it, sometimes for hours. A seven o'clock refresh may happen at ten past nine. For Mandi Bhav that is fine, and that is the point: if your work cannot tolerate that, it needs a different tool, and the next two lessons say which.

The emulator was not charging during these tests (AC powered: false); Doze never starts on a charging phone.

App Standby Buckets

Android sorts every app into a bucket by how recently and how often it is used, and rations background work by bucket. The figures Google publishes, as of November 2026:

Bucket Typical app Regular jobs Network for jobs
Active In use now, or tapped from a notification a moment ago Up to 20 minutes in a rolling hour Yes
Working set Used most days Up to 10 minutes in 4 hours Yes
Frequent Used often, not daily Up to 10 minutes in 12 hours Yes
Rare Seldom used Up to 10 minutes in 24 hours No
Restricted Unused for 8 days (Android 13+), or misbehaving Once a day, 10 minutes, batched with other apps No

The emulator agrees with the first rows: dumpsys jobscheduler lists qc_allowed_time_per_period_active_ms=1200000 (20 minutes) and qc_allowed_time_per_period_working_ms=600000 (10). To see a bucket bite, the same experiment as Doze, with the app moved to the lowest bucket by hand:

adb shell am set-standby-bucket com.riztechacademy.mandibhav restricted
Unsatisfied constraints: CONNECTIVITY WITHIN_QUOTA [0x11000000]
Standby bucket: RESTRICTED

Two minutes past its due time, nothing had run. Moving the app back with am set-standby-bucket com.riztechacademy.mandibhav active, and the worker started 0.3 seconds later. The rare bucket gave the same two unmet constraints. A farmer who installs Mandi Bhav at harvest and forgets it in the off-season will drift down these buckets, and her daily refresh will drift with them. That is not a bug to fix; it is the system working as designed. What you control is that the work is scheduled properly, so that it runs when the system allows.

Force-stop

Force-stop is the one the user chooses, and Android takes it literally. Before and after adb shell am force-stop com.riztechacademy.mandibhav, counting the app's jobs in dumpsys jobscheduler:

registered jobs for com.riztechacademy.mandibhav: 1
registered jobs for com.riztechacademy.mandibhav: 0

and the package is marked stopped=true. Nothing will start it: no job, no alarm, no broadcast from another app. The next time Priya opens Mandi Bhav, WorkManager notices and puts the work back:

D/WM-ForceStopRunnable: Application was force-stopped, rescheduling.
D/WM-SystemJobScheduler: Scheduling work ID 62fcbfa9-… (com.riztechacademy.mandibhav.work.PriceRefreshWorker), Job ID 1

Swiping the app away is not a force-stop, and the difference matters when you test. Use am force-stop only to test force-stop; use the Recents screen, or adb shell am kill, to test what normal users do.

Choosing the tool

The work Use Why
Must happen eventually, can wait: a daily refresh, an upload of logs WorkManager Survives process death and reboots, respects Doze and buckets, retries
Must happen soon, short, user-visible result: sending a message the user has written WorkManager expedited work Runs at once if the system's quota allows
Long, started by the user, must not be interrupted: navigation, a call, music A foreground service of the right type A visible notification buys the right to keep running (lesson 3)
At an exact clock time the user set: an alarm clock, a calendar reminder AlarmManager.setExactAndAllowWhileIdle or setAlarmClock Fires in Doze; needs a special permission on recent Android
Triggered by a server: a new message, a price that changed Firebase Cloud Messaging One shared connection for every app; wakes the app briefly
Only while the user is looking The screen's own scope, WhileSubscribed Module 9's sync on reconnect

Mandi Bhav's refresh is the first row. It can wait an hour; it must not be lost. That is precisely what WorkManager is for, and the next lesson builds it.

The trap: testing on your own phone, on charge, in your hand

Every restriction in this lesson relaxes when the phone is charging, on the screen, or in the Active bucket. A developer's phone is all three, all day. Background code that "works fine on my phone" has usually never met Doze, a low bucket or a swipe. The commands in this lesson are how you meet them on purpose.

Check your work

Why module 9's application scope does not deliver a seven o'clock refresh. It lives only as long as the process, and once the app is off the screen Android freezes or kills the process; in the swipe test the download did not finish either way.

What Doze did to the refresh. It cut the network for background work, so the job waited with CONNECTIVITY unmet until Doze ended; then it ran within three seconds.

What the Restricted bucket did. Left the job waiting on CONNECTIVITY and WITHIN_QUOTA; moved back to Active, it ran at once.

What a force-stop does to scheduled work. Removes every job (1 became 0) and marks the app stopped until the user opens it.

Why startService from the background fails. Since Android 8, an app in the background may not start an ordinary service; it throws BackgroundServiceStartNotAllowedException.

Practice 3. On Android 16, am get-standby-bucket printed 10, the number for Active, right after the app was opened. The public values are 10 (active), 20 (working set), 30 (frequent), 40 (rare) and 45 (restricted); the experiments above saw 40 and 45 when the app was moved by hand.

Practice

  1. Repeat the swipe experiment with module 9's code: a local server that waits eight seconds (module 9 lesson 3 showed the five-line change), tap "Refresh", swipe the app away from Recents, and read adb logcat for remove task and freezing. Then reopen the app and see whether the prices arrived.
  2. Run adb shell dumpsys jobscheduler com.riztechacademy.mandibhav and find, among the output, the five qc_allowed_time_per_period_… lines. Match each to the table above.
  3. Run adb shell am get-standby-bucket com.riztechacademy.mandibhav right after opening the app. Write down the number and look it up.
  4. Force-stop the app, then run adb shell dumpsys package com.riztechacademy.mandibhav and find stopped=. Open the app and look again.
  5. Break it on purpose: in a scratch copy, start an ordinary Service from a BroadcastReceiver, send the broadcast with adb shell am broadcast -n <package>/<receiver> while the app is in the background, and read the exception. Then send it while the app is on screen. What changes, and why?
  6. Harder: list three features of apps on your own phone that run in the background (a backup, a step counter, a chat), and for each, pick the row of the "Choosing the tool" table it belongs in. For each, say what the user would notice if Doze delayed it by two hours.

Put any setting you change back afterwards: dumpsys deviceidle unforce, and am set-standby-bucket <package> active.

Official documentation

Next: WorkManager, and the daily refresh that survives all of this.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship