Building a persistent foreground service on Android
Android has long given developers a way to keep work alive after the activity disappears, yet the rules around background execution keep shifting. When you need a task to stay running while the user switches apps, takes a call, or moves between networks, a foreground service is the standard pattern. It signals to the system that the work is user-visible and worth protecting from aggressive reclamation.
In Australia, where field technicians drive between mine sites in the Pilbara, tradies upload job reports from rural Queensland, and ride-share drivers in Brisbane rely on constant location pings, the cost of a service being killed mid-task is real. A background work order might stall in the middle of uploading sensor data, costing time and money. That's why learning how to implement a long-running foreground service properly remains a relevant skill for anyone building for the local market.
What a foreground service actually does
A foreground service is a started service that the app promotes to user-aware status by calling startForeground(). Once promoted, the service holds a high priority and must display a persistent notification that the user cannot dismiss. The trade-off is transparency: the user always knows something is happening, and Android agrees not to kill the process casually.
This differs from a bound service, which lives only while components are attached to it, and from WorkManager, which schedules deferrable work without any guarantee of immediacy. If you need a continuous upload, an active audio stream, or a fitness tracker recording every step during a Sydney Harbour Bridge climb, a foreground service is the right tool.
Manifest declarations and runtime permissions
Before you can build the class itself, the manifest needs the right declarations. For Android 9 (API 28) and above, you must include the FOREGROUND_SERVICE permission. From Android 14 (API 34), the system further demands a typed permission such as FOREGROUND_SERVICE_LOCATION or FOREGROUND_SERVICE_DATA_SYNC, matching the kind of work the service performs.
The service element in AndroidManifest.xml must declare android:foregroundServiceType, exported=false where appropriate, and any permissions used inside the service. A media playback service, for example, asks for FOREGROUND_SERVICE_MEDIA_PLAYBACK, while a sensor logger may need FOREGROUND_SERVICE_HEALTH. Without these declarations, calling startForeground() throws a SecurityException at runtime and the process crashes.
Writing the service class itself
Inside the class, override onStartCommand() and return START_STICKY so Android recreates the service when memory frees up. Construct a NotificationChannel once at app startup, build a Notification with a small icon and ongoing flag, and call startForeground(NOTIFICATION_ID, notification) within five seconds of service creation. Failing this five-second rule is a common reason services get shut down on newer Android releases.
The work itself runs in a coroutine, a HandlerThread, or an Executor depending on the task. Keep CPU-bound work off the main thread and store progress in a shared StateFlow or LiveData so the UI can observe without polling. Below is a compact comparison of the common options for background work on Android.
| Mechanism | Best for | Lifespan guarantee | User-visible notification |
|---|---|---|---|
| Foreground Service | Continuous, user-facing tasks (audio, location, sync) | High, until stopped or app revoked | Required |
| WorkManager | Deferrable, retryable jobs | Survives reboot, no immediacy | No |
| JobScheduler | OS-managed background scheduling | Best-effort, system-batched | No |
| Bound Service | Tasks tied to an active client component | Lives only while bound | No |
Promoting the service and managing lifecycle
To start the service from an Activity or another component, build an explicit Intent with the service class and call ContextCompat.startForegroundService(this, intent). From Android 12 onwards, you cannot launch a foreground service while the app is in the background without an exemption, which matters for apps that try to kick off a sync when the user closes them. Common exemptions include high-priority FCM messages and certain media playback actions.
Inside the service, expose start, pause, and stop actions through the Intent and route them to a single command handler. Avoid leaking the service by registering and unregistering receivers or callbacks in onCreate and onDestroy. Use ServiceCompat to manage the foreground promotion portably across API levels, and call stopSelf() when the work is complete so the system can clean up the notification.
Modern restrictions and what survives Doze
Android's battery saver, Doze mode, and App Standby buckets all try to reduce background activity. A foreground service keeps the app in the active bucket and exempts it from most Doze restrictions, which is why location pings for a fleet management tool in remote Western Australia stay reliable while the phone screen is off. Even so, manufacturers like Samsung and OPPO add aggressive battery managers on top of stock Android, and users in regional areas sometimes toggle these optimisations on to squeeze out a few extra hours between charges.
Test the service with battery saver enabled, with the device in Doze by running adb shell dumpsys deviceidle force-idle, and after an app standby cycle. If the service drops out, post a periodic notification update rather than relying on a single long-lived one and avoid wakelocks unless absolutely required. For memory allocation patterns when working with bitmaps inside long services, it helps to review guidance on heapalloctolocalallocno-weiito-xuan-ze-ji-zhun, since continuous tasks often hold onto large bitmaps far longer than a short activity would.
When the task finishes, call stopForeground(STOP_FOREGROUND_REMOVE) and stopSelf() to tear everything down cleanly. A well-behaved foreground service keeps the user informed, survives the normal lifecycle bumps, and steps out of the way the moment its work is done.