Google Play Ecosystem
The Wake Lock Enforcement
March 2026: Google made battery drain a Play Store quality gate. Apps with excessive wake locks got a warning label and lost recommendation placement. What patterns got apps flagged?
The Incident
On March 1, 2026, Google began enforcing the Excessive Partial Wake Lock metric as a Play Store quality gate. Apps where more than 5% of user sessions accumulated 2+ cumulative hours of CPU wake lock in 24 hours received a red warning label on their store listing and were removed from recommendation surfaces. The enforcement surfaced a class of anti-patterns that had existed for years — invisible until Google made them a store-quality metric.
Evidence from the Scene
- Play Console: apps above 5% excessive sessions received a 'may use more battery than expected' warning label
- Affected apps were removed from discovery recommendations — direct impact to organic installs
- The most common pattern: WorkManager CoroutineWorkers that suspended indefinitely waiting for state
- Second pattern: PeriodicWorkRequests configured to run every 15 minutes (minimum interval) for tasks that could run hourly
- Third pattern: PowerManager.WakeLock.acquire() called without a timeout parameter
- All affected workers had one thing in common: they could suspend or block longer than WorkManager's intent
The Suspects
2 of these are the real root causes. The others are plausible-sounding distractors.
CoroutineWorkers suspending indefinitely on StateFlow.first() or similar operators when preconditions weren't met
PowerManager.WakeLock.acquire() called without a timeout — wake lock never released on failure path
PeriodicWorkRequest set to 15-minute intervals for tasks that could safely run hourly or daily
WorkManager tasks running without setRequiresBatteryNotLow(true) during low battery
SensorManager registered with maxReportLatencyUs = 0, disabling sensor hardware batching
The Verdict
Real Root Causes
CoroutineWorkers suspending indefinitely on StateFlow.first() or similar operators when preconditions weren't met
StateFlow.filterNotNull().first() or Channel.receive() inside a CoroutineWorker's doWork() suspends the coroutine — and WorkManager's internal wake lock — until an emission arrives. If the emission never comes (device offline, sensor disconnected, state permanently null), the worker holds the wake lock until WorkManager's maximum execution time and reschedules, repeating indefinitely.
PowerManager.WakeLock.acquire() called without a timeout — wake lock never released on failure path
acquire() without a timeout parameter creates a wake lock that must be manually released. If the code path to release() is guarded by a condition that isn't met (exception, early return, state mismatch), the wake lock is held forever. The fix: always use acquire(timeoutMillis) or wrap in try/finally.
Plausible But Wrong
PeriodicWorkRequest set to 15-minute intervals for tasks that could safely run hourly or daily
Over-scheduled periodic work increases wake frequency and cumulative battery cost — a real concern — but it produces many short workers, not the 2+ hour session accumulation that triggers the excessive wake lock metric. The indefinite suspension pattern is the dominant cause of that metric.
WorkManager tasks running without setRequiresBatteryNotLow(true) during low battery
Missing battery constraints allow work to run when battery is low — a consideration for scheduling strategy, but not the cause of the excessive wake lock metric. The metric is about session duration of wake lock accumulation, not scheduling frequency.
SensorManager registered with maxReportLatencyUs = 0, disabling sensor hardware batching
Disabling sensor batching increases CPU wake frequency for sensor data delivery — a secondary battery concern. The excessive partial wake lock metric is primarily driven by workers that suspend indefinitely, not sensor sampling frequency.
Summary
Google's March 2026 enforcement turned battery drain from a user complaint into a store-quality gate with direct business consequences: warning labels and lost recommendation placement. The most common pattern flagged was CoroutineWorkers encoding 'wait for state' logic instead of 'check and exit' logic — causing indefinite coroutine suspension and unbounded wake lock accumulation. The architectural fix: workers must check preconditions synchronously and return Result.failure() or Result.retry() immediately if not met. Never suspend inside a worker waiting for external state to change. WHOOP's published fix (same month) became the reference implementation.
The Real Decision That Caused This
“Writing WorkManager workers that wait for conditions to become true inside doWork() rather than checking conditions and exiting — turning units of work into indefinite event listeners that hold CPU wake locks.”
Lesson Hint
Chapter 6 (Concurrency) covers structured concurrency, coroutine cancellation, and StateFlow replay semantics. Chapter 7 (Platform & Performance) covers WorkManager patterns, battery optimization, and Google Play's quality enforcement.
Want to test yourself before reading the verdict?
Open Interactive Case in Autopsy Lab