Reliable Android Background Tasks: Alarms, Doze, and Battery Safety
A technical deep dive into scheduling reliable background work on Android without destroying battery life or getting killed by the OS.
When you build an app that relies on timing, you are fighting a two-front war. On one side, you have the user who expects a notification to fire exactly when they need it. On the other side, you have the Android operating system, which is aggressively trying to put your app to sleep to save battery life. For most apps, a delay of ten minutes is an invisible inconvenience. But for tools like Salah Companion: Prayer Times, where a reminder must align with a specific astronomical event, or Trophy Tykes, where a child’s routine depends on a specific morning trigger, being 'close enough' isn't enough.
Learning to navigate the labyrinth of Android’s power management requires moving past basic documentation. You have to understand the philosophy of the OS: Google prioritizes the device's longevity over your app's punctuality. If you want to break that rule, you have to prove your case to the system and the user. This is a guide to the trade-offs, the APIs, and the hard-learned lessons of scheduling work that actually runs.
The Hierarchy of Reliability
Android provides several ways to run code when your app isn't in the foreground. Developers often reach for the most precise tool first, but that is a recipe for having your app flagged as a battery drain. You should choose your tool based on how much the user will suffer if the task is delayed by ten minutes.
- 1.WorkManager: The default choice. It is persistent, survives reboots, and handles constraints like 'only run when charging.' However, it is not for precision. It operates on a 'best effort' basis.
- 2.Inexact Alarms (AlarmManager): Better for windows of time. You tell the system 'run this roughly at 2:00 PM.' Android will batch your request with other apps to wake the radio and CPU only once.
- 3.Exact Alarms (AlarmManager): The nuclear option. You demand the CPU wake up at a specific millisecond. This requires special permissions and is subject to heavy scrutiny by the Play Store.
Comparison: When to Use Which Tool
| Requirement | Recommended Tool | Impact on Battery | Reliability |
|---|---|---|---|
| Syncing a local database | WorkManager | Low (Batched) | High (Persistent) |
| Daily habit reminder | Inexact Alarm | Medium | Medium (Doze-sensitive) |
| Prayer time / Medical alert | Exact Alarm | High | Highest |
| Fetching stock data | WorkManager | Low | High |
Surviving Doze Mode and App Standby
Introduced in Android 6.0 and tightened in every version since, Doze Mode is the primary antagonist of background tasks. When the screen is off and the device is stationary, the system enters a deep sleep. It shuts down network access, ignores partial wake locks, and—most importantly—defers all alarms.
Doze works in 'maintenance windows.' During these brief windows, the system lets apps catch up on their work. If your alarm triggers while the device is in Doze, it is pushed to the next window. These windows become less frequent the longer the phone sits idle.
To bypass this for mission-critical tasks, you must use setExactAndAllowWhileIdle. This method tells Android: 'I know the device is sleeping, but this specific task cannot wait for the next maintenance window.' Use this sparingly. If you use it for every minor update, the system's battery stats will eventually point the finger at you, and the user will uninstall your app.
The Exact Alarm Permission Headache
Starting with Android 12, the SCHEDULE_EXACT_ALARM permission became a hurdle. You can no longer assume you have it. You must declare it in your manifest, and on newer versions of Android, the user can revoke it in system settings. For Android 13 and 14, Google intensified this by requiring apps to meet specific use cases (like calendars or alarms) to even use the permission without it being considered a policy violation.
Before you call an exact alarm, you must check canScheduleExactAlarms(). If it returns false, you need to gracefully degrade your experience or prompt the user to grant the permission in settings.
The 'Hard Way' Lesson: Never call setExact without a fallback. We learned during the development of our scheduling logic that if you fail to check the permission, the app simply crashes with a SecurityException. Your code should look like this:
- 1.Check if precision is actually required for this specific instance.
- 2.Check if the permission is granted.
- 3.If granted, use
setExactAndAllowWhileIdle. - 4.If not, use an inexact alarm and inform the user that timing might be slightly off due to battery settings.
Handling Reboots and 'Turned Off' States
Alarms scheduled via AlarmManager are cleared when a device is turned off. If a user’s phone dies overnight and they plug it in at 8:00 AM, all your carefully calculated schedules are gone.
To fix this, you need to listen for the BOOT_COMPLETED broadcast. This requires the RECEIVE_BOOT_COMPLETED permission. When the device starts, your BroadcastReceiver wakes up, and you must re-calculate and re-schedule all necessary alarms.
However, there is a catch: if the user 'Force Stops' your app from the settings menu, your app enters a 'stopped' state. In this state, nothing—not even a BOOT_COMPLETED broadcast—will wake it up until the user manually opens the app again. This is a common source of support tickets where users claim 'the app just stopped working.' Educating users on the difference between closing an app and force-stopping it is part of the job.
Foreground Services: The Necessary Evil
Sometimes, you don't just need to trigger a task; you need to keep it running. If your app is doing active work in the background—like playing audio, tracking a workout, or running a timed interview in Employ Ready—you must use a Foreground Service.
This shows a persistent notification to the user. It is Android’s way of saying, 'This app is using your battery right now, and here is why.' Since Android 14, you must specify the 'type' of foreground service (e.g., dataSync, location, mediaPlayback). If your service doesn't fit one of these types, Google may reject your app.
Testing the Un-testable
You cannot wait 24 hours to see if a daily alarm triggers. You need to use the Android Debug Bridge (ADB) to force the system into different states. We use these commands daily to verify our logic:
- Force Doze Mode:
adb shell dumpsys deviceidle force-idle - Exit Doze Mode:
adb shell dumpsys deviceidle unforce - Trigger App Standby Buckets:
adb shell am set-standby-bucket <package_name> rare
Testing in the 'Rare' bucket is crucial. Apps you don't use often are put in this bucket, which heavily restricts how many alarms they can fire. If your app works perfectly while you are developing it (in the 'Active' bucket) but fails for the user after three days of disuse, the standby bucket is the likely culprit. For more on how to manage your own focus while tackling these complex engineering problems, see our post on Focus methods that survive contact with a real workday.
Checklist for Background Reliability
Before you push your scheduling code to production, run through this checklist:
- [ ] Do you really need millisecond precision? If a 5-minute delay is acceptable, use
WorkManager. - [ ] Have you handled `BOOT_COMPLETED`? Ensure alarms are rescheduled after a restart.
- [ ] Are you checking `canScheduleExactAlarms()`? Avoid crashes on Android 12+.
- [ ] Is your `PendingIntent` flag correct? Use
FLAG_IMMUTABLEby default, orFLAG_MUTABLEonly if necessary. - [ ] Have you tested in Doze mode? Use the ADB commands to verify the alarm fires while the device is 'idle'.
- [ ] Did you add a 'Battery Optimization' prompt? For mission-critical apps, you may need to ask the user to exclude the app from battery optimizations manually.
What to do next
- 1.Audit your current alarms. Identify any instances where you are using
setExactand determine ifWorkManageror inexact alarms could suffice to save the user's battery. - 2.Implement a Reboot Receiver. If your app relies on
AlarmManager, ensure you have aBroadcastReceiverthat restores those alarms after the device restarts. - 3.Test your app in the 'Rare' bucket. Use ADB to place your app in the most restrictive standby bucket and see if your notifications still arrive. This is the ultimate test of your background architecture.