Doze, App Standby, and Battery Buckets Explained

AndroidKotlinBackground WorkPerformance
Share on LinkedIn

If your background sync works perfectly in testing and then users complain it "stops overnight," you've met Doze. Doze mode and App Standby are Android's two battery-saving mechanisms that throttle background work, and they operate on different axes: Doze restricts the whole device when it's idle, while App Standby restricts individual apps based on how often they're used. Understanding both is the difference between sync that quietly degrades and sync you can reason about.

I've spent real time chasing "it only fails on the user's device" bugs that all traced back to these systems. Once you internalize the model, they stop being mysterious and start being a spec you design against.

Doze: the device goes to sleep

When a device is unplugged, screen off, and stationary for a while, it enters Doze. In Doze the system batches deferred work into periodic maintenance windows that grow further apart the longer the device stays idle — a few minutes at first, then tens of minutes, then hours. Between windows:

There's also a lighter "Doze on the go" that kicks in when the device is moving (in a pocket) but the screen is off — same idea, shallower restrictions. The mental model: the device isn't dead, it's checking its inbox on a schedule that gets lazier the longer nobody touches it.

The escape hatches are deliberately narrow. High-priority FCM messages still get through for genuinely time-sensitive pushes. setExactAndAllowWhileIdle and setAndAllowWhileIdle can fire during Doze but are rate-limited. Foreground services are exempt while running. Everything else waits for a maintenance window.

App Standby: per-app buckets

Doze is about the device; App Standby is about your app specifically. The system continuously sorts each app into a bucket based on recency and frequency of use, and the bucket determines how aggressively that app's jobs and alarms are throttled:

Bucket Roughly means Job/alarm treatment
Active In use right now No restrictions
Working set Used regularly Mild deferral
Frequent Used often, not daily More deferral, capped
Rare Rarely used Heavy deferral, strict caps
Restricted Misbehaving / almost never used Once-a-day job window, tight limits

The restricted bucket is the one that hurts: an app there might get a single job execution window every 24 hours. Apps land there by being ignored by the user or by triggering the system's abuse heuristics (excessive wakeups, long background wakelocks). The lesson is that being a good background citizen literally keeps you in a better bucket.

Why your alarms drift

Combine the two and the behavior falls out. A plain periodic alarm on a rarely-used app, on a device deep in Doze, can be delayed by hours — Doze defers it to a maintenance window, and the app's bucket caps how often those windows serve it. This is exactly why exact alarms on Android 14 require a special permission: piercing this system is a privilege, not a default.

Designing work that survives

The practical playbook I follow:

  1. Use WorkManager for anything that tolerates delay. It schedules through JobScheduler, which is Doze- and bucket-aware, so your work runs in maintenance windows instead of being silently dropped. Add constraints (NetworkType.CONNECTED, requiresCharging) and let the system pick a good moment.
  2. Use high-priority FCM for time-sensitive delivery. If the user must know now (a message, a ride request), don't try to poll through Doze — push it. A high-priority data message wakes the app briefly even in Doze.
  3. Reserve exact alarms for wall-clock events. Alarm clocks and countdowns, not sync.
  4. Batch and coalesce. Fewer, larger wakeups keep you out of the abuse heuristics and out of the restricted bucket.
  5. Don't beg for exemptions. REQUEST_IGNORE_BATTERY_OPTIMIZATIONS exists but Play restricts it to narrow categories, and users distrust it. Design to cooperate instead.

Testing it for real

You cannot find these bugs on a plugged-in emulator, because charging disables Doze. Force the states with adb:

# Simulate Doze
adb shell dumpsys deviceidle force-idle
adb shell dumpsys deviceidle unforce

# Force the worst App Standby bucket
adb shell am set-standby-bucket com.your.app restricted
adb shell am get-standby-bucket com.your.app

Run your sync flow under forced Doze and the restricted bucket, and watch what actually executes. Every serious background bug I've shipped a fix for was reproducible this way and invisible without it.

What I'd take away

Doze throttles the whole device when it's idle; App Standby throttles your app based on how much the user actually uses it — and they compound. Stop trying to run background work on your own schedule and start cooperating: WorkManager for deferrable jobs, high-priority FCM for truly urgent delivery, exact alarms only for wall-clock events, and batching to stay out of the penalty buckets. Then test against forced Doze and a restricted bucket before you ship, because that's the environment a real user's phone lives in overnight.

Log UsageStatsManager.getAppStandbyBucket() in beta builds — users in restricted bucket are your canary for background sync failures.

Document expected background behavior per standby bucket in your app's troubleshooting guide — support teams otherwise escalate "notifications stopped" as bugs when Android is working as designed.

Resources

Frequently asked questions

What is the difference between Doze and App Standby?

Doze is device-wide: when the screen is off and the device is stationary and unplugged, the whole system enters low-power windows that defer background work and network access for every app. App Standby is per-app: the system sorts each app into a usage bucket and throttles that specific app's jobs and alarms based on how recently the user interacted with it. Doze is about the device state, buckets are about the individual app.

How do I test Doze mode during development?

Use adb to force it. Run 'adb shell dumpsys deviceidle force-idle' to push the device into Doze and 'adb shell dumpsys deviceidle unforce' to leave it. For App Standby, 'adb shell am set-standby-bucket <package> restricted' lets you simulate the worst bucket. Testing against forced Doze catches the deferred-work bugs that never appear on a plugged-in test device.

Does WorkManager work during Doze?

Yes — WorkManager schedules through JobScheduler, which is Doze-aware, so your work runs during the periodic maintenance windows rather than being dropped. It won't run at an exact instant during a deep Doze period, but it will run, and it respects constraints like network and charging. For anything that tolerates a delay, WorkManager is the right tool precisely because it cooperates with Doze instead of fighting it.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →