Services
A Service is a lifecycle component with no screen. It is not a thread, and since Android 8 it is not a way to run forever in the background either.
The most consequential sentence in the Services documentation is a negative: a service runs in the main thread of its hosting process. Everything confusing about services follows from people expecting otherwise.
TL;DR#
- A is a component with a lifecycle and no UI. It is not a thread and it does not create one.
- Started services run until
stopSelf()orstopService(). Bound services live while at least one client is bound. - Since Android 8.0, background services are stopped shortly after the app leaves the foreground. Use a foreground service or .
- A foreground service must show a notification and declare a type since Android 14.1
- Binding is the in-process or cross-process API surface:
IBinder, , and the client'sServiceConnection.
Mental model#
A service is a contractor with a work order.
A started service is a job you hand over: "paint the hallway". The contractor keeps working whether or not you are home, until the job is done or you cancel it. A bound service is a standing arrangement: the contractor is only on site while at least one client is paying for the day; when the last client leaves, they go home.
Where the analogy breaks#
The contractor here works in your kitchen, at your table, with your tools. A service's callbacks run on the app's main thread, in the app's process, with the app's UID. It is not a separate worker that the system schedules elsewhere — it is a lifecycle around code that runs where your app already runs. The only way it becomes another process is if you declare it that way in the manifest.
The big picture#
Text description of this diagram
A sequence diagram of an activity start, which shows the same process and component machinery that applies to a service. The caller crosses Binder into ActivityTaskManagerService in system_server, which resolves the intent with PackageManagerService, checks export and permission, and asks ActivityManager to start a process through Zygote if the target app has none. The new process binds its Application, and only then does ActivityThread instantiate and drive the component. For a service the equivalent server-side path is ActivityManagerService.startService, but the process creation, UID drop, application bind and main-thread dispatch are identical.
The parallel is deliberate. Whether the target is an activity or a service, the interesting work —
resolution, permission checks, process creation — happens in system_server and in Zygote, not in
your app.
Deep dive#
Started versus bound#
class SyncService : Service() {
override fun onBind(intent: Intent): IBinder? = null // started-only service
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val id = intent?.getStringExtra(EXTRA_ACCOUNT) ?: return START_NOT_STICKY
// onStartCommand runs on the main thread.
enqueueSync(id)
return START_NOT_STICKY
}
override fun onDestroy() {
cancelSync()
super.onDestroy()
}
}The return value from onStartCommand is a policy statement:
| Return | Meaning |
|---|---|
START_STICKY | restart the service later, with a null intent |
START_NOT_STICKY | do not restart; the work was not worth resuming |
START_REDELIVER_INTENT | restart and re-deliver the last intent |
START_STICKY_COMPATIBILITY | sticky, but with no guarantee about re-delivery |
Most modern code returns START_NOT_STICKY and lets WorkManager own retry policy.
A bound service is a different animal. It returns a Binder from onBind, and clients receive it in
onServiceConnected:
class PlayerService : Service() {
private val binder = LocalBinder()
private val player = ExoPlayer.Builder(this).build()
inner class LocalBinder : Binder() {
fun service(): PlayerService = this@PlayerService
}
override fun onBind(intent: Intent): IBinder = binder
fun play(track: Track) {
// Must be called on the main thread, or post to it.
player.setMediaItem(track.mediaItem)
player.prepare()
player.play()
}
}
class PlayerActivity : ComponentActivity() {
private var service: PlayerService? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
service = (binder as PlayerService.LocalBinder).service()
}
override fun onServiceDisconnected(name: ComponentName) {
service = null
}
}
override fun onStart() {
super.onStart()
bindService(Intent(this, PlayerService::class.java), connection, BIND_AUTO_CREATE)
}
override fun onStop() {
unbindService(connection)
super.onStop()
}
}BIND_AUTO_CREATE is what makes the service live exactly as long as the binding. That pairing of
bindService in onStart and unbindService in onStop is the canonical pattern.
The background execution limits#
This is the part that changed the design of most apps. Before Android 8.0, a started service could run indefinitely. From 8.0, once an app goes into the background, the system stops its background services after a short grace period and does not let it start new ones.2
That pushed work into three places:
- Foreground service — a service with a visible notification, allowed to keep running.
WorkManager— deferrable work with constraints, guaranteed to run eventually.JobScheduler— the platform API underneathWorkManager, for jobs the system should batch.
The decision rule is short. If the user is actively aware of the work and it must continue now, use a
foreground service. If it can wait for conditions but must not be lost, use WorkManager. Almost
nothing else needs a plain started service in 2026.
Foreground services, and their types#
A foreground service calls startForeground() with a notification within a few seconds of starting.
If it does not, the system throws and kills the app.
Since Android 14, a foreground service must also declare a type that describes what it does:
camera, location, microphone, mediaPlayback, dataSync, connectedDevice and others. The
type is declared in the manifest and passed to startForeground:
<service
android:name=".UploadService"
android:foregroundServiceType="dataSync"
android:exported="false" />override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildProgressNotification()
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
enqueueUpload()
return START_NOT_STICKY
}The type is not decoration. It is what the system checks against your permissions — a camera type
requires camera permission — and what the user sees in the task manager and in battery settings.
bindService details that bite#
val intent = Intent(this, PlayerService::class.java)
val flags = Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT
val bound = bindService(intent, connection, flags)
if (!bound) {
// The system refused: no such service, or a permission failure.
}Four flags change the lifetime semantics:
| Flag | Effect |
|---|---|
BIND_AUTO_CREATE | create the service if it is not running |
BIND_IMPORTANT | treat the client as foreground while bound |
BIND_WAIVE_PRIORITY | do not raise the service's process priority |
BIND_NOT_FOREGROUND | do not let the binding make the service foreground |
The return value of bindService is easy to ignore and matters: false means the binding failed
immediately, and onServiceConnected will never be called. Treating it as fire-and-forget produces a
service that silently never connects.
BIND_IMPORTANT is what a music app uses while playing: even if the activity is in the background, the
bound service keeps a foreground-ish priority. It is not a substitute for startForeground on modern
Android, but it does affect how the process is ranked.
Cross-process binding#
A bound service in another process returns a Binder proxy, not the real object. The interface must be described in AIDL:
// IRemoteSync.aidl
interface IRemoteSync {
void requestSync(String account);
int pendingCount();
}The generated Stub handles parceling and thread dispatch. Two rules matter:
- Calls arrive on a Binder thread, not your main thread. Do not touch UI without posting.
- A synchronous call blocks the caller. Prefer
onewayfor fire-and-forget methods, and keep transactions small — the transaction buffer is a fixed budget shared by the whole process.
Service lifetime versus process lifetime#
A started service keeps its process alive, but not indefinitely, and not at a fixed priority. The system ranks a process by its most important component, and a background service is a middling rank: above a cached process, below a foreground one.
That is why a long-running background service on Android 8+ gets stopped: not because the component is forbidden, but because the process rank it implies is no longer high enough to justify the memory.
// The honest way to run work that must continue.
override fun onCreate() {
super.onCreate()
startForeground(NOTIFICATION_ID, buildNotification())
}A foreground service raises the process rank and requires the notification. That trade — visibility for continuity — is the platform's actual policy, and every "my service gets killed" question reduces to it.
Services are not background threads#
The single most common misunderstanding, and worth restating with evidence. onStartCommand,
onBind, onCreate and onDestroy all run on the main thread of the hosting process. A service that
does heavy work in onStartCommand will ANR within seconds.
// Wrong: this blocks the main thread of the app.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val result = api.syncBlocking() // network call on the main thread
save(result)
return START_NOT_STICKY
}
// Right: hand the work to a structured-concurrency scope.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
scope.launch {
val result = withContext(Dispatchers.IO) { api.syncBlocking() }
save(result)
stopSelf(startId)
}
return START_NOT_STICKY
}Note stopSelf(startId): passing the start ID prevents a race where a newer start command is stopped
by an older job finishing.
Debugging a service that will not start#
Three causes, in order of frequency, and each has a distinct symptom:
startForegroundService without startForeground. The service starts and is killed within
seconds with a RemoteServiceException in logcat. The fix is a notification within the deadline.
Background start restriction (API 26+). The service is never created, and startService throws
IllegalStateException if the app is in the background. The fix is a foreground service or
WorkManager.
A component not exported, reached from another app. The call fails with a SecurityException
naming the component, which at least points straight at the cause. Checking dumpsys activity services
shows whether the service was ever created.
When not to use a service at all#
If the work is short and tied to the UI, do it in the UI's lifecycle scope. If it must survive the
screen, put it in a ViewModel scope. If it must survive the process, it needs WorkManager or a
foreground service — a plain service gives you no such guarantee on modern Android.
How it evolved#
| 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 |
|
Each row of that table removed a way for an app to run invisibly. The service component survived by becoming explicit: foreground types, user-visible notifications, and a type that must match the work.
Under the hood#
- Service.javathe service base class and its start/stop/bind contract
- ActiveServices.javasystem_server's bookkeeping for started and bound services
- ServiceConnection.javathe client-side binding callback
- ServiceConnection.javathe interface a binding client implements
Curiosity corner#
- A service can be destroyed and restarted with a null intent. If you return
START_STICKY, be ready forintent == nullinonStartCommand. Many crashes come from assuming otherwise. bindServicecan fail without callingonServiceConnected. If the service crashes during creation, you getonBindingDiedoronNullBinding, not a connection.- A foreground service is not exempt from the ANR timer. It is exempt from being stopped, not from being responsive.
Try it#
adb shell dumpsys activity services com.example.notes
adb shell am startservice -n com.example.notes/.UploadService
Check yourself#
Check yourself
You call `startActivity()` for an activity in another app. Which check does the system perform before delivering the intent?
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.
- docsForeground services overview | Background workdeveloper.android.comaccessed 2026-10-27↩
- docsServices overview | Background workdeveloper.android.comaccessed 2026-10-27↩
- docsFeatures and APIs overview | Android 12developer.android.comaccessed 2026-10-27↩
- docsProcesses and threads overview | App qualitydeveloper.android.comaccessed 2026-10-27↩
- docsAnrdeveloper.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 16 or higherdeveloper.android.comaccessed 2026-10-27↩
- docsWorkmanagerdeveloper.android.comaccessed 2026-10-27↩