majorstaff2026

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