Battery Optimization on Android: How the System Actually Manages Power

1. The power management stack

Android's battery behavior is the product of four layers acting together, and most user-facing "optimization" advice only touches the top one:

Everything below is AOSP behavior unless the OEM layer is called out explicitly.

2. Doze mode

Doze is triggered by DeviceIdleController when the device is unplugged, screen off, and stationary (confirmed via the significant motion sensor) for a period of time. It runs in two nested tiers.

Light Doze

Triggers faster — screen off and unplugged is close to sufficient, without requiring stationarity. Network access is throttled but not fully suspended, and maintenance windows recur on a short cycle. This is the tier most phones spend the majority of idle screen-off time in.

Deep Doze

Requires the stationary condition. The device cycles between an idle state and short maintenance windows on an exponential backoff (roughly starting at minutes, extending toward the ~1 hour range on later cycles, capped and reset by the device on a full wake or motion event). During idle:

Note

Doze allowlisting ("Unrestricted" battery setting, or dumpsys deviceidle whitelist) exempts an app from network suspension and job deferral during Doze, but does not exempt it from App Standby Bucket throttling — those are separate mechanisms with separate exemption paths.

3. App Standby Buckets

Introduced in Android 9, buckets throttle background activity per-app based on predicted usage, independent of Doze's device-wide state. AppStandbyController assigns a bucket using a combination of usage recency, explicit user interaction, and — on devices with Adaptive Battery enabled — an on-device ML model trained on usage patterns.

BucketTypical triggerJob/alarm throttlingFCM impact
activeApp in foreground or recently interacted withNoneImmediate delivery
working_setUsed regularly, not currently activeJobs deferred; ~2 alarm windows/hourNear-immediate
frequentUsed often but not daily-driverFurther job batching; ~1 window/hourSlight delay possible
rareInfrequently usedJobs batched to ~1 window per several hours; network access limited to daily windowsDelayed, batched with other traffic
restrictedUnused for an extended period, or user force-stopped repeatedlyJob execution effectively disabled outside rare batched windows; no background network by defaultHigh-priority messages only, heavily delayed

Buckets are re-evaluated continuously; an app can move from rare back to active the moment the user opens it. The restricted bucket is the one most responsible for "notifications arrive late" complaints, and it's usually a self-inflicted state from the user force-stopping the app repeatedly rather than a system default.

Checking an app's current bucket

# requires a debug build or root for full output on some OEM skins
adb shell am get-standby-bucket com.example.app

# full standby state dump
adb shell dumpsys usagestats | grep -A 3 "package: com.example.app"

4. Background execution limits (Android 8+)

Independent of Doze and Standby, Oreo introduced hard limits that apply whenever the app is not in the foreground:

JobScheduler and its WorkManager wrapper are the sanctioned replacement for background Service polling: the system batches jobs from multiple apps into shared wake windows instead of letting each app wake the radio and CPU independently.

5. Wakelocks

A wakelock is a kernel-level hold that prevents the SoC from entering a deep idle state. The framework exposes them via PowerManager.WakeLock; the kernel tracks them by name in /sys/power/wake_lock (held) and /sys/power/wake_unlock (released).

TypeEffectStatus
PARTIAL_WAKE_LOCKKeeps CPU running, screen and radio can still sleep independentlyStandard for background work
SCREEN_DIM_WAKE_LOCKKeeps screen on dim + CPUDeprecated API 17
SCREEN_BRIGHT_WAKE_LOCKKeeps screen at full brightness + CPUDeprecated API 17
FULL_WAKE_LOCKScreen on, keyboard bright, CPU runningDeprecated API 17, avoid
Danger

A partial wakelock acquired without a timeout and never released is the single most common cause of anomalous overnight drain. It survives Doze's normal wakelock-ignoring behavior if the app was allowlisted, and it prevents deep idle even outside Doze. Always check acquire(timeout) usage when profiling a specific app.

# list currently held wakelocks and their duration
adb shell dumpsys power | grep -A2 "Wake Locks"

# kernel-level wakelock accounting (aggregate time held per source)
adb shell cat /sys/kernel/debug/wakeup_sources

6. AlarmManager

Exact alarms are expensive because they force a wake regardless of batching opportunity. The API surface pushes toward inexactness by default:

MethodDoze behaviorBatching
set()Deferred to next maintenance windowBatched with other inexact alarms
setExact()Deferred to next maintenance windowNot batched, but still deferred
setAndAllowWhileIdle()Fires during idle, rate-limitedNo
setExactAndAllowWhileIdle()Fires during idle at exact time, rate-limitedNo
setAlarmClock()Always fires exactly, exempts app from Doze brieflyNo — reserved for user-facing alarms

7. Radio power states

Cellular and Wi-Fi radios don't transition instantly between off and active. LTE/5G modems follow an RRC (Radio Resource Control) state machine: idle → a high-power connected state on data activity → a lingering "tail" at high power for several seconds after the last packet, before stepping back down. That tail is why five small requests spaced a minute apart cost meaningfully more energy than one batched request carrying the same payload — each one re-triggers the ramp-up and tail.

This is the underlying reason push (FCM) beats polling: a single persistent connection amortizes the ramp cost across all apps using it, instead of each app independently waking the radio on its own schedule.

8. Location power cost

Priority (FusedLocationProviderClient)SourceRelative power cost
PRIORITY_HIGH_ACCURACYGPS + Wi-Fi + cellHighest
PRIORITY_BALANCED_POWER_ACCURACYWi-Fi + cell (~block-level)Moderate
PRIORITY_LOW_POWERCell only (~city-level)Low
PRIORITY_PASSIVEPiggybacks on other apps' requests onlyNegligible

Geofencing offloaded to the hardware geofence chip (where present) costs far less than an app polling getCurrentLocation() on a timer, since the comparison runs on a low-power coprocessor instead of waking the applications processor.

9. OEM background management

Several Android skins — most notably MIUI, EMUI/Magic UI, ColorOS, and older One UI versions — layer an additional, non-AOSP background killer on top of Doze/Standby. These use heuristics outside the documented framework contract and are the leading cause of missed notifications on non-Pixel/AOSP-adjacent devices, because they can kill a process even while it holds a valid Doze allowlist exemption.

Warn

Granting an app "Unrestricted" battery status in Android settings only affects the AOSP-layer mechanisms (Doze, Standby, background execution limits). It does not disable a manufacturer's separate background app manager — that typically requires a second, OEM-specific toggle (e.g. autostart permission, or an explicit exclusion list in the phone manager app).

10. Diagnostics

# snapshot of current battery state, level, temperature, plug status
adb shell dumpsys battery

# reset accumulated battery stats to start a clean measurement window
adb shell dumpsys batterystats --reset

# force the system to treat the device as unplugged for testing Doze
adb shell dumpsys battery unplug

# step the device through Doze states manually
adb shell dumpsys deviceidle step

# check current Doze state
adb shell dumpsys deviceidle get deep

# pull a full bugreport for Battery Historian ingestion
adb bugreport report.zip

Battery Historian renders the bugreport's batterystats section as a timeline — wakelock hold periods, radio state transitions, and job execution windows plotted per app — which is the fastest way to confirm a suspected culprit rather than guessing from the Settings battery usage percentage breakdown, which only shows foreground/background split, not the mechanism responsible.

11. User-facing settings and what they actually do

SettingUnderlying mechanism
Battery SaverReduces background CPU/network priority, disables/limits location, caps refresh rate and animations, restricts vibration and always-on display
Adaptive BatteryEnables the on-device ML model driving App Standby Bucket assignment; without it, buckets fall back to simpler recency heuristics
App battery usage: UnrestrictedDoze allowlist exemption + exempt from background execution limits
App battery usage: Optimized (default)Full Doze + Standby Bucket enforcement
App battery usage: RestrictedForces the app toward the restricted standby bucket regardless of usage pattern
Adaptive brightnessDisplay power draw scaling — largest single component of screen-on power on OLED panels at high brightness
Always-on displayPeriodic partial-panel refresh; on OLED, cost scales with lit pixel area, not flat
Tip

Force-stopping an app from Settings or a task killer does not reduce battery use over time the way people expect — it clears the app's process and any in-memory state, but doesn't change its standby bucket favorably, and repeated force-stops are one of the triggers that push an app into the throttled restricted bucket, making its background behavior worse, not better.