Wakelocks and the art of sleeping
A phone must sleep to survive, and must wake to be useful. The machinery that decides which is a story about a rejected kernel patch, a userspace dialect, and the cheapest way to keep a CPU alive.
Suspend is the single most consequential thing a phone kernel does. Every millisecond the device can stay asleep is battery saved; every millisecond it wakes for no reason is battery spent. Between those two states sits a coordination problem: dozens of independent components — the modem, the display, an app downloading a file, a music player — all need to say "not yet" or "now I'm done", and the kernel has to combine their answers into one decision.
Android's first answer to that problem, the wakelock, became one of the most argued-about patches in Linux history. Understanding why it was rejected, and what replaced it, is a good way to understand how Android and Linux negotiate.
TL;DR#
- Suspend puts the CPU and most devices into a low-power state; resume brings them back. A phone spends most of its life suspended and wakes many times a minute.
- A wakelock is a request from a component saying "keep the system awake until I release this". Android's original in-kernel implementation was never accepted upstream.
- instead grew wakeup sources: a generic mechanism where a kernel subsystem or a user-space program can hold a wakeup event, and the PM core refuses to suspend while any is active.
- Modern Android routes sleep decisions through SystemSuspend, a system service that aggregates wake locks and decides when the device may suspend. It has existed since Android 10 and replaced the older in-process suspend thread.
- The framework layer above it is PowerManagerService: app wake locks, the screen state, and the suspend blocker all feed into the same decision. Doze then makes the whole device defer work when nothing needs it.
Mental model#
Picture a shared office at night.
At the end of the day everyone is supposed to go home and the lights go off. In practice, people stay late. So the building installs a simple rule: the lights stay on while anyone is still in the office, and turn off when the last person leaves. Each person has a badge they can put on a board when they arrive and take off when they leave. The board is the count.
A wakelock is a badge. The building's rule — lights on while badges are on the board — is the suspend decision. If someone forgets to take their badge off, the lights stay on all night, and the electricity bill shows it.
The hard part is not the rule. It is trusting the people. A badge can be lost, or left on purpose. A process can die while holding a lock, and then nobody is in the office and the lights are still on.
Where the analogy breaks#
The office has one rule. A phone has several overlapping ones. The kernel's wakelocks, the framework's PowerManager locks, the display's own power state, the modem's wakeup sources and the battery saver's restrictions all influence the outcome, and they are not nested neatly.
There is also a cost asymmetry the office analogy misses: a mis-set wakelock does not just waste power, it can also keep the device awake during an update or prevent Doze from ever starting, which makes the whole battery story worse. And unlike the office, the phone usually wants to wake up — to deliver a notification, finish a download — so the decision is not "sleep as much as possible" but "sleep when nothing legitimately needs the CPU".
The big picture#
Text description of this diagram
A left-to-right pipeline of the boot sequence, from the SoC boot ROM through the bootloader, the kernel, first-stage init, second-stage init, zygote, system_server, and finally SystemUI and the launcher. Each stage hands control to the next and produces something the next stage needs: the bootloader produces a verified kernel and ramdisk, the kernel produces init running as PID 1, first-stage init produces mounted system and vendor partitions, second-stage init produces running core services, zygote produces a process template, system_server produces registered Binder services, and the launcher produces the first frame.
That diagram is here for a different reason than boot. Look at the stage where zygote starts: init's zygote-start trigger runs exec_start update_verifier and then start zygote, and the zygote service definition carries an onrestart rule that writes to /sys/power/wake_lock. Suspend and boot are wired together at the very bottom of the system.1
Deep dive#
What suspend actually is#
The kernel's system sleep is controlled through a small set of files in /sys/power/. Writing a string to /sys/power/state requests a sleep state:
| Value | Meaning |
|---|---|
freeze | Suspend-to-idle: user space is frozen, devices go to low power, CPUs enter deep idle. Software only. |
mem | Interpreted according to /sys/power/mem_sleep, which selects s2idle, shallow or deep |
disk | Hibernation: memory contents written to storage |
The important one on a phone is the combination of freeze and a deep idle state: the CPU stops executing, the display is off, most peripherals are powered down, and only a small set of interrupts can wake the system. Everything that must happen while the device is "off" — an incoming call, a BLE advertisement, an alarm — is driven by one of those wakeup interrupts.
Writing to /sys/power/state is a privileged operation. On a stock device only a system process can do it, and SELinux enforces that. That is deliberate: if any app could suspend the machine, any app could break the phone.
The wakelock idea, and why Linux said no#
Android's original wakelocks, introduced with the first devices, were a kernel API with a user-space interface. A driver called wake_lock() to keep the system awake, wake_unlock() to release it, and user space wrote a name to /sys/power/wake_lock and /sys/power/wake_unlock to do the same.
The design was proposed to the Linux kernel mailing list in 2009 and met sustained resistance. The objections were technical and, in hindsight, correct:
- The kernel already had runtime power management, and wakelocks duplicated it with a different vocabulary.
- A wakelock was associated with a process, and there was no reliable recovery if that process died while holding one. The device could stay awake until reboot.
- The interface conflicted conceptually with
/sys/power/state: two mechanisms, two mental models, one machine. - Wakelocks were suspend-centric. Linux was moving toward a model where devices manage their own power continuously, and suspend is just one more state.
The resolution took years and went sideways.2 Mainline gained a wakeup source infrastructure, and in 2012 it gained a userspace interface on top of it: /sys/power/wake_lock and /sys/power/wake_unlock, modelled explicitly on Android's. The idea — userspace can hold a reason for the system to stay awake — was adopted. The implementation — an Android-only driver with its own semantics — was not.
From libsuspend to SystemSuspend#
Before Android 10, the suspend loop lived in a library thread, libsuspend. It would read /sys/power/wakeup_count, check whether any wake locks were held, and write mem to /sys/power/state if the answer was no.
Android 10 replaced it with a service, SystemSuspend. The architecture is worth following because it is a clean example of moving policy out of a library:
- A main thread answers Binder calls. Native clients acquire a wake lock and get back a unique handle; releasing is one-way.
- A suspend thread loops. It reads
/sys/power/wakeup_count, acquires a mutex, waits until the count of outstanding wake locks reaches zero, writes the value it read back towakeup_count, and — if that write succeeds — writesmemto/sys/power/state.
The wakeup_count dance is the subtle part. Reading it gives you a snapshot of how many wakeup events have occurred. Writing the same value back succeeds only if no new wakeup event has happened in between; if one has, the write fails and the loop retries. That closes a race that would otherwise let the system suspend after an interrupt had already fired, losing the event.
Two interfaces sit on top. ISystemSuspend is what native clients use; it hands out a unique IWakeLock per client, so Binder death cleans up automatically — a property the old name-based /sys/power/wake_lock interface never had. ISuspendControlService is for system_server only, and exposes enableAutosuspend, registerCallback and forceSuspend.
The framework layer: PowerManagerService#
Above SystemSuspend sits PowerManagerService in system_server, and above that sits the API apps actually use: PowerManager.WakeLock.
The API is small and mostly stable:
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val lock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp:Sync")
lock.setReferenceCounted(false)
lock.acquire(10 * 60 * 1000L) // timeout in milliseconds
try {
// work that needs the CPU to stay awake
} finally {
if (lock.isHeld) lock.release()
}Four details in that snippet matter more than the syntax.
PARTIAL_WAKE_LOCK is deprecated, along with the screen-related levels. It still works, but the modern guidance is to avoid holding a lock for screen behaviour entirely and use the window flag FLAG_KEEP_SCREEN_ON instead.
acquire() without a timeout is deprecated. The timeout form exists because an app that acquires and then crashes without releasing would otherwise keep the CPU awake. A timeout is a bound on the damage.
setReferenceCounted(false) makes the lock behave like a simple on/off switch rather than a counter. With the default, two acquire() calls need two release() calls; with false, one release is enough. The latter is harder to get wrong.
The permission is android.permission.WAKE_LOCK, a normal permission granted at install. It is not a runtime permission, and it is not dangerous on its own — the danger is the battery, which is why the Play Store and the platform both police usage rather than access.
Doze: the platform's answer to bad locks#
Because the platform cannot trust every app to release its locks promptly, Android 6.0 introduced Doze. When the device is unplugged, stationary and screen-off for a while, the system enters a state where it:
- suspends network access,
- ignores wake locks,
- defers
AlarmManageralarms to the next maintenance window, - stops Wi-Fi scans,
- does not run sync adapters,
- does not run
JobSchedulerjobs — and therefore notWorkManagerwork, which is built on it.
Periodically the device briefly leaves Doze in a maintenance window, runs everything pending, and goes back. Over time the windows become less frequent.3
Doze is the clearest statement of the design philosophy: a wakelock is a request, not a right. When the device judges that nothing urgent is happening, it overrides the request.
Where the kernel actually helps: PSI and cgroups#
Suspend and memory pressure are different problems, but they share a shape: the kernel reports a condition, user space decides what to do. Modern Android uses for both memory and, increasingly, for scheduling decisions.
PSI exposes three files:
/proc/pressure/cpu
/proc/pressure/memory
/proc/pressure/ioEach contains lines like:
some avg10=0.42 avg60=0.31 avg300=0.18 total=4823911
full avg10=0.00 avg60=0.00 avg300=0.00 total=0some is the share of time at least one task was stalled; full is the share of time all non-idle tasks were stalled, which is the state where real work is not happening. The avg10/avg60/avg300 values are percentages over ten, sixty and three hundred seconds.4
Cgroup v2 exposes the same data per group, as memory.pressure, cpu.pressure and io.pressure, and adds the controllers Android relies on: memory for accounting and limits, cpu for bandwidth, io for throttling, pids for fork limits.5 The cgroup filesystem is where the framework's process grouping becomes something the kernel can reason about.
How it evolved#
The suspend story has three eras, and they roughly track Android's relationship with upstream Linux.
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 17 | 4.2 Jelly Bean |
|
| 19 | 4.4 KitKat |
|
| 21 | 5.0 Lollipop |
|
| 26 | 8.0 Oreo |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 34 | 14 Upside Down Cake |
|
| 35 | 15 Vanilla Ice Cream |
|
| 36 | 16 Baklava |
|
2008–2012: Android's own. Wakelocks in the kernel, a suspend thread in user space, and a patch set that upstream would not take.
2012–2019: the mainline interface. CONFIG_PM_WAKELOCKS and wakeup sources. The user-space interface became a mainline feature, even though Android's driver remained.
2019 onward: the service. Android 10 moved suspend into SystemSuspend; the framework talks to a service, not a sysfs file. Doze, introduced in Android 6.0, had already made the platform willing to override app requests.
Under the hood#
- init.rcwake_lock and wake_unlock permissions, and the zygote restart rule that takes a lock
- SystemSuspend.cppthe suspend thread and the wakeup_count protocol
- PowerManagerService.javawhere app wake locks become a suspend decision
- init.zygote64.rcthe zygote service's onrestart wake lock
Curiosity corner#
- The suspend race is a classic. Reading
wakeup_countand writing it back is a compare-and-swap against the kernel. If it is not atomic, a wakeup that arrives between the read and the write would be lost — and the device would sleep through its own alarm. - A process dying while holding a lock was the original objection. Binder's death notification is what finally made the problem solvable in user space: the service learns the client died and releases its handle.
- Doze ignores wake locks on purpose. That is not a bug in your app's lock; it is the platform deciding that a stationary, idle phone has no urgent work.
Try it#
adb shell dumpsys power | head -40
adb shell dumpsys power | grep -i wakelock
adb shell cat /sys/power/wakeup_count
adb shell dumpsys deviceidle force-idle
adb shell dumpsys deviceidle unforce
Example output
mWakeLockSummary=0x00000001 (PARTIAL_WAKE_LOCK)
mWakefulness=Awake
mIsPowered=false
mLastSleepTime=+1h24m13s
...
PARTIAL_WAKE_LOCK 'MyApp:Sync' (uid 10234) ACQ 12sdumpsys power is the most useful command in this area, because it shows both halves: the framework's summary of who is holding locks, and the underlying state. If mWakefulness never reaches Asleep, something is holding the device awake and the rest of the output tells you what.
Check yourself#
Check yourself
Ashmem, Android's original shared-memory driver, was replaced by which standard Linux mechanism?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- sourcesystem/core/rootdir/init.rcandroid.googlesource.comaccessed 2026-10-27↩
- docsAndroid kernel overviewsource.android.comaccessed 2026-10-27↩
- docsOptimize for Doze and App Standbydeveloper.android.comaccessed 2026-10-27↩
- docsPressure Stall Information (PSI)docs.kernel.orgaccessed 2026-10-27↩
- docsControl Group v2docs.kernel.orgaccessed 2026-10-27↩