How to Stop Your Android App from Draining the Battery with Wake Locks
A wake lock is one of the simplest APIs in Android and one of the easiest to misuse. Acquiring one takes a single line of code. Forgetting to release it can keep a phone's CPU awake for hours, drain the battery overnight, and earn your app a warning label on Google Play.
Most wake lock bugs aren't caused by a misunderstanding of the API itself. They come from ordinary control flow problems: an exception thrown between acquire() and release(), an early return, a callback that never fires, or a reference count that drifts out of balance.
These bugs are hard to spot in code review and invisible during normal testing, because the device is usually plugged in and awake anyway.
This article explains what a wake lock actually does at the system level, walks through the most common ways apps leak them, and shows concrete patterns and tools for preventing and detecting leaks. It also covers the modern APIs that remove the need for manual wake locks in most cases.
Table of Contents
- Prerequisites
- What Happens When an Android Device Sleeps
- What a Wake Lock Does
- The Types of Wake Locks
- How to Acquire and Release a Wake Lock
- How Reference Counting Works
- Common Ways Apps Leak Wake Locks
- Patterns That Prevent Leaks
- When You Don't Need a Wake Lock at All
- How to Detect Wake Lock Leaks
Prerequisites
To follow along, you should have:
- Basic familiarity with Android development in Kotlin or Java
- Android Studio installed (Hedgehog or newer recommended)
- A physical device or emulator running Android 10 (API 29) or higher
- Working knowledge of
PowerManager, background execution limits, and Android's Doze mode
What Happens When an Android Device Sleeps
When an Android device is idle, the system aggressively conserves power. Two mechanisms are at play:
- Display sleep โ the screen turns off after a period of inactivity.
- CPU sleep โ once background work finishes, the application processor enters a low-power state. Timers stop firing, network callbacks pause, and your app simply stops running.
- A wake lock does not turn the screen on (unless you use a screen wake lock, which is rarely appropriate).
- A wake lock does not guarantee your app keeps running โ the system can still kill your process under memory pressure.
- A wake lock does not bypass Doze. Since Android 6.0,
PARTIALWAKELOCKis ignored during Doze unless your app is exempt or running a foreground service. - Wake locks are reference-counted per tag, not per object. This is where most leaks originate.
- Always use a unique, descriptive tag. It appears in
dumpsys poweroutput, which is essential for debugging. - Always release in a
finallyblock. An exception betweenacquire()andrelease()will leak. - Prefer
acquire(timeout)over indefinite acquisition. A timeout guarantees the lock is eventually released even if your code path fails. - Acquiring twice with the same tag and releasing once leaves the CPU awake forever โ a classic leak.
- Releasing more times than you acquired throws a
RuntimeExceptionunderPowerManager's strict mode. - Background sync, uploads, retries โ
WorkManager. It acquires and releases wake locks internally, respects Doze and battery saver, and handles process death. - Location updates while moving โ foreground service with
locationtype. The system keeps the CPU running while the foreground service is active. - Music playback, VoIP calls โ foreground service with the appropriate type.
- Long-running user-visible work โ foreground service with a clear
foregroundServiceTypeand a persistent notification. dumpsys powerโ shows live locks with tags and reference counts.- Android Studio Profiler (Energy) โ visualizes wake lock acquisitions over time against app activity.
- Battery Historian โ parses a bugreport and shows wake lock duration alongside screen and CPU state. A wake lock that spans screen-off periods with no matching app work is a leak.
This is intentional. If every app could keep the CPU running indefinitely, standby battery life would collapse. Android's Doze mode, introduced in Android 6.0 (API 23) and refined through Android 15 and Android 16, makes this even stricter: apps in the background are deferred, batched, and often paused entirely until the device exits Doze.
In 2026, the platform is stricter than ever. Android 15 introduced tighter foreground service timeouts, and Android 16 continues to push background work into WorkManager and JobScheduler. If you're still holding a manual wake lock for routine background tasks, you're fighting the platform.
What a Wake Lock Does
A wake lock is a request to the PowerManager system service to keep the device in a specific power state. The most common type, PARTIALWAKELOCK, keeps the CPU running while allowing the screen and keyboard backlight to turn off.
Important characteristics:
For a comprehensive list of changes, see the Android 16 behavior changes documentation.
The Types of Wake Locks
PowerManager exposes several wake lock levels:
| Flag | Keeps CPU awake | Keeps screen on | Typical use |
|---|---|---|---|
| PARTIALWAKELOCK | โ
| โ | Background processing, sync, downloads |
| SCREENDIMWAKE_LOCK | โ
| Dim | Rarely used; deprecated behavior |
| SCREENBRIGHTWAKELOCK | โ
| Bright | Deprecated; use FLAGKEEPSCREENON instead |
| FULLWAKELOCK | โ
| Bright | Deprecated; avoid |
| PROXIMITYSCREENOFFWAKELOCK | โ
| Off | Phone calls only |
For screen-on behavior, prefer WindowManager.LayoutParams.FLAGKEEPSCREENON on a view. For background work, use PARTIALWAKE_LOCK โ or better, use WorkManager, which handles wake locks for you.
How to Acquire and Release a Wake Lock
A minimal example looks like this:
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
val wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"MyApp::SyncWakeLock"
)
wakeLock.acquire()
try {
performSync()
} finally {
wakeLock.release()
}
Key points:
wakeLock.acquire(10 * 60 * 1000L) // 10 minutes
try {
performSync()
} finally {
if (wakeLock.isHeld) {
wakeLock.release()
}
}
The isHeld check prevents an RuntimeException from releasing an already-released lock under some conditions.
How Reference Counting Works
Wake locks are reference-counted per tag, not per WakeLock instance. If two parts of your code acquire a lock with the same tag, the count is 2. Each call to release() decrements by one, and the CPU only sleeps when the count reaches zero.
This has two consequences:
Because the counter is per tag, two unrelated components using the same tag can interfere with each other. Use namespaced tags like "MyApp::SyncWorker" and "MyApp::LocationUpdates" to keep ownership clear.
Common Ways Apps Leak Wake Locks
1. Exceptions between acquire and release
The most common cause. Any exception thrown in the work block skips release().
// BAD
wakeLock.acquire()
doRiskyWork() // may throw
wakeLock.release() // never runs on exception
Fix: wrap in try/finally.
2. Early returns
A guard clause in the middle of the work block returns before release() is reached.
// BAD
wakeLock.acquire()
if (cacheFresh()) return
fetchFromNetwork()
wakeLock.release()
Fix: use use {} with an AutoCloseable wrapper, or try/finally.
3. Callbacks that never fire
If you acquire a lock before a network callback and the callback never fires (timeout, cancellation, process death mid-flight), the lock leaks. Always pair network-driven wake locks with a timeout.
4. Lock held across process death (or lack thereof)
Wake locks are tied to the process. If your process is killed, the lock is released automatically. But if you expected the work to continue after process death โ for example, in a BroadcastReceiver โ the CPU may sleep before your work completes. Use goAsync() with a timeout, or better, delegate to WorkManager.
5. Multiple acquire calls with the same tag
As described above, reference counting means repeated acquire() calls stack. A leak of one reference keeps the device awake forever.
6. Static or singleton wake lock references
A static wake lock reused across activities or services makes ownership ambiguous. When one component releases, another may still expect the lock to be held.
Patterns That Prevent Leaks
Wrap wake locks in an `AutoCloseable`
Kotlin's use block guarantees release, even on exceptions:
class ScopedWakeLock(
context: Context,
tag: String,
timeoutMs: Long = 10 * 60 * 1000L
) : AutoCloseable {
private val wakeLock = (context.getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag)
init {
wakeLock.acquire(timeoutMs)
}
override fun close() {
if (wakeLock.isHeld) wakeLock.release()
}
}
// Usage
ScopedWakeLock(context, "MyApp::Sync").use {
performSync()
}
Always set a timeout
An indefinite wake lock is a liability. A timeout transforms a potential overnight battery drain into a bounded, recoverable condition.
Centralize wake lock ownership
If multiple components need wake locks, route them through a single manager that tracks ownership and enforces timeouts. This makes audits easier and prevents tag collisions.
Monitor with `dumpsys power`
During development, run:
adb shell dumpsys power | grep -A 20 "Wake Locks"
You'll see every active lock, its tag, and its reference count. A lock held by your app with no active work is a leak.
When You Don't Need a Wake Lock at All
In 2026, most use cases that historically required PARTIALWAKELOCK are better served by higher-level APIs:
Manual wake locks should be reserved for narrow cases: short, low-level hardware interactions inside an already-active foreground service, or bridging a very brief gap where the CPU must not sleep between a broadcast and a scheduled job.
If you find yourself reaching for PARTIALWAKELOCK in a modern codebase, treat it as a code smell and check whether WorkManager or a foreground service fits.
How to Detect Wake Lock Leaks
Three tools cover almost every case:
For automated CI checks, consider lint rules that flag WakeLock.acquire() calls without a matching release() in the same scope, and unit tests that assert isHeld == false after every code path.
Wake locks are not evil โ they are a precise tool for a narrow job. The problem is that they look harmless in code review and behave invisibly at runtime. Wrap them in scoped helpers, always set a timeout, prefer WorkManager and foreground services where they fit, and verify with dumpsys power before you ship. Your users' batteries โ and your Play Store rating โ will thank you.
via FreeCodeCamp
