We migrated our auto app backup feature from a 24/7 foreground service to WorkManager periodic workers, significantly improving battery life while maintaining reliable backups. This post shares our journey adapting to Android's increasingly strict background execution policies.
The Challenge: Android's War on Background Services
If you've been developing Android apps for a while, you've witnessed the platform's progressive tightening of background execution. What started as gentle warnings in Android 8.0 (Oreo) has evolved into strict enforcement by Android 14+.
For our All Backup & Restore app, this created a critical problem: How do you automatically backup newly installed or updated apps when Android won't let you run in the background?
The Old Way: Foreground Service (2020-2024)
How It Worked
Our initial implementation was straightforward:
// Broadcast Receiver listening for boot
class BootCompleteReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "android.intent.action.BOOT_COMPLETED") {
// Start foreground service
context?.startForegroundService(serviceIntent)
}
}
}
The Service would:
- Start as a foreground service with persistent notification
- Register broadcast receivers for PACKAGE_ADDED and PACKAGE_REPLACED
- Immediately backup any newly installed/updated app
- Run 24/7 until user disables the feature
Architecture Diagram (Old):
↓
Start Foreground Service
↓
Show Persistent Notification
↓
Register Broadcast Receivers
↓
Listen for PACKAGE_ADDED/REPLACED
↓
Immediate Backup Triggered
The Problems Started Rolling In
1. Android 14 Crashes (Late 2024)
Fatal Exception: ForegroundServiceStartNotAllowedException
Unable to start service - not allowed to start foreground service
Android 14 introduced FOREGROUND_SERVICE_TYPE_DATA_SYNC requirements. We had to add specific permissions and service type declarations in the manifest.
But the crashes kept coming.
2. Battery Drain Complaints
User reviews started mentioning:
- "App drains battery even when I'm not using it"
- "Why is there always a notification?"
- "Can't you just run when needed?"

3. System Killing the Service
Despite being a foreground service, Android would still kill it on:
- Low memory situations
- Battery saver mode
- Manufacturer-specific optimizations (Samsung, Xiaomi, OnePlus)
We added restart logic, but it felt like fighting the platform.
The Turning Point: Mid-2025
After analyzing production data showing:
- Significant portion of Android 14 users experiencing service crashes
- Noticeable battery consumption from our background service
- User retention dropping due to service-related issues
We decided: Stop fighting the system. Work with it.
The New Way: WorkManager Periodic Polling
Architecture Redesign
Instead of listening for app changes in real-time, we poll for changes periodically.

Core Components
1. MonitorWorker - The Detector
This worker runs periodically and:
- Loads the previously known list of installed apps (from persistent storage)
- Gets the current list of installed apps from PackageManager
- Compares both lists to detect new installations or updates
- Schedules individual backup workers for changed apps
- Saves the current app list for the next comparison
Key Logic:
// Pseudo-code for clarity
fun detectChanges(lastKnown: List<AppInfo>, current: List<AppInfo>): List<AppInfo> {
val changes = mutableListOf<AppInfo>()
for (app in current) {
val oldApp = lastKnown.find { it.packageName == app.packageName }
when {
oldApp == null -> changes.add(app.copy(isNew = true))
app.versionCode > oldApp.versionCode -> changes.add(app.copy(isUpdate = true))
}
}
return changes
}
2. BackupWorker - The Executor
This worker handles the actual backup:
- Receives package name as input
- Performs the backup operation
- Returns success/failure result
3. Scheduling Logic
// Scheduling periodic monitoring
val workRequest = PeriodicWorkRequestBuilder<MonitorWorker>(
1, TimeUnit.HOURS // Check every hour
)
.setConstraints(
Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true)
.build()
)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"app_monitor_work",
ExistingPeriodicWorkPolicy.KEEP,
workRequest
)
Architecture Diagram (New):
↓
Schedule MonitorWorker (Every 1 hour)
↓
Worker Wakes Up
↓
Load Last Known Apps from Storage
↓
Get Current Installed Apps
↓
Compare & Detect Changes
↓
For Each Changed App:
Schedule BackupWorker (Staggered delays)
↓
Save Current Apps List
↓
Worker Sleeps (No notification, no battery drain)

The Evolution: From Real-Time to Periodic
Timeline of Changes
| Period | Change | Reason |
|---|---|---|
| Late 2024 | Added Android 14 service type | Crash fixes |
| Mid 2024 | Started investigating alternatives | Battery complaints |
| Mid 2025 | Switched to WorkManager | Complete redesign |
| Late 2025 | Optimized interval (15min → 1 hour) | Reduce overhead |
| Recent | Fixed coroutine scope issues | Stability improvements |
Key Optimizations We Made
1. Staggered Worker Execution
Problem: When 20 apps update at once, starting 20 backup workers simultaneously caused ANR (Application Not Responding).
Solution: Stagger delays by 20 seconds per app.
// Pseudo-code
changedApps.forEachIndexed { index, app ->
val delaySeconds = index * 20L
// App 0: immediate, App 1: 20s, App 2: 40s, etc.
scheduleBackup(app, delaySeconds)
}
Result: Maximum 1-2 concurrent workers, no ANR.
2. Structured Concurrency Fix
Problem: Certain backup operations used unstructured coroutine scopes which caused cancellation exceptions.
Solution: Properly scope coroutines within parent job hierarchy.
// Before (Problematic)
fun saveBundleApp() = CoroutineScope(Dispatchers.IO).async {
// Orphaned scope - not connected to parent lifecycle
}
// After (Fixed)
suspend fun saveBundleApp(): Result = coroutineScope {
try {
val result = async { /* backup logic */ }.await()
result
} catch (e: Exception) {
// Proper error handling
Result.failure(e)
}
}
Result: Eliminated cancellation-related crashes in production.
3. Smart Constraints
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true)
.build()
Workers don't run when:
- Battery < 15%
- Storage critically low
Before (Foreground Service)
- ⚡ Battery impact: Significant daily drain
- 📱 Persistent notification: Always visible
- ⏱️ Backup delay: Immediate (0-5 seconds)
- 💥 Crash rate: Notable on Android 14+
- 👥 User satisfaction: Lower ratings mentioning battery
After (WorkManager)
- ⚡ Battery impact: ~70% reduction
- 📱 Persistent notification: None (only during active backup)
- ⏱️ Backup delay: Up to 1 hour
- 💥 Crash rate: 95% reduction
- 👥 User satisfaction: Improved ratings, fewer battery complaints

Trade-offs and Learnings
What We Gained
✅ Battery Efficiency - Dramatic reduction in battery usage
✅ Better UX - No annoying persistent notifications
✅ Platform Compliance - Works with Android, not against it
✅ Stability - Significant reduction in crashes
✅ Future-Proof - Compatible with future Android versions
What We Sacrificed
⚠️ Real-Time Detection - Up to 1 hour delay (acceptable for backup use case)
⚠️ Polling Overhead - Checks every hour even if no changes (minimal CPU)
Key Learnings
-
Stop Fighting the Platform
Android's restrictions exist for good reasons (battery, security). Work with them. -
Polling > Listening in Modern Android
For non-critical tasks, periodic polling is more reliable than broadcast receivers. -
WorkManager is Production-Ready
Despite initial skepticism, WorkManager handles edge cases better than custom solutions. -
Structured Concurrency Matters
Orphaned coroutine scopes will cause production issues. Always use structured concurrency. -
Test on Real Devices
Manufacturer-specific optimizations behave differently than emulators.
Migration Guide for Other Apps
If you're facing similar issues, here's our recommended approach:
Step 1: Audit Current Background Usage
Analyze:
- Battery consumption patterns
- Crash reports related to background services
- User feedback about battery/notifications
Step 2: Identify Real-Time vs Delayed Requirements
Ask: "Does this NEED to happen immediately, or can it wait 15-60 minutes?"
For most background tasks, delays are acceptable.
Step 3: Implement WorkManager
// Periodic task example
val workRequest = PeriodicWorkRequestBuilder<YourWorker>(
15, TimeUnit.MINUTES // Minimum interval
).build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"unique_work_name",
ExistingPeriodicWorkPolicy.KEEP,
workRequest
)
Step 4: Add Constraints
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true)
.build()
)
Step 5: Monitor and Optimize
- Track worker success/failure rates
- Monitor battery impact
- A/B test different intervals
- Adjust based on user feedback
Technical Considerations
Data Persistence
Store the app list using:
- SharedPreferences for small datasets
- Room Database for larger datasets
- Kotlin Serialization for easy JSON conversion
Comparison Algorithm
Efficient comparison is key:
// Use maps for O(1) lookup
val lastKnownMap = lastKnown.associateBy { it.packageName }
// Single pass comparison
current.forEach { app ->
val oldVersion = lastKnownMap[app.packageName]?.versionCode ?: 0
if (app.versionCode > oldVersion) {
// App is new or updated
}
}
Worker Chaining
For complex operations:
val monitor = OneTimeWorkRequestBuilder<MonitorWorker>().build()
val backup = OneTimeWorkRequestBuilder<BackupWorker>().build()
WorkManager.getInstance(context)
.beginWith(monitor)
.then(backup)
.enqueue()
Common Pitfalls to Avoid
-
Too Frequent Polling
Checking every 1-5 minutes defeats the purpose. Stick to 15min minimum. -
Not Using Constraints
Always add battery and storage constraints. -
Ignoring WorkManager Guarantees
WorkManager will execute your work eventually, but timing isn't guaranteed. -
Forgetting to Handle Cancellation
Always implement proper cancellation handling in workers. -
Not Testing Edge Cases
Test with airplane mode, battery saver, and doze mode enabled.
Conclusion
Migrating from foreground services to WorkManager wasn't just about compliance—it made our app genuinely better. Users are happier, battery life improved, and we eliminated an entire class of crashes.
If you're still using foreground services for periodic background tasks, it's time to migrate. The platform has evolved, and so should we.
The key insight: Modern Android rewards apps that respect system resources. WorkManager is the platform's way of saying "we'll help you do background work efficiently—just follow our guidelines."