Zygote
Components & Intentsintermediate

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.

11 minAPI 1–37 · 17interactivequizverified 2026-10-27

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() or stopService(). 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's ServiceConnection.

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#

Loading diagram…
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:

ReturnMeaning
START_STICKYrestart the service later, with a null intent
START_NOT_STICKYdo not restart; the work was not worth resuming
START_REDELIVER_INTENTrestart and re-deliver the last intent
START_STICKY_COMPATIBILITYsticky, 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:

  1. Foreground service — a service with a visible notification, allowed to keep running.
  2. WorkManager — deferrable work with constraints, guaranteed to run eventually.
  3. JobScheduler — the platform API underneath WorkManager, 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:

FlagEffect
BIND_AUTO_CREATEcreate the service if it is not running
BIND_IMPORTANTtreat the client as foreground while bound
BIND_WAIVE_PRIORITYdo not raise the service's process priority
BIND_NOT_FOREGROUNDdo 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 oneway for 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#

Background and power changes12 releases
APIVersionWhat changed
11.0
  • The original android.* SDK: Activity, Service, BroadcastReceiver, ContentProvider
174.2 Jelly Bean
  • Daydream service
194.4 KitKat
  • AlarmManager becomes inexact by default for targeting apps
215.0 Lollipop
  • JobScheduler
  • Implicit service intents restricted for targeting apps
268.0 Oreo
  • Background service limits
289 Pie
  • Foreground service permission
2910
  • Background activity starts restricted
3011
  • Background location requires a second request
3112
  • Exact alarms need SCHEDULE_EXACT_ALARM
  • Foreground service launch restrictions from the background
3414 Upside Down Cake
  • Foreground service types required when targeting 34
  • User-initiated data transfer jobs
3515 Vanilla Ice Cream
  • Deprecated APIs: WindowInsetsController hiding, elevate the importance of foreground services
3616 Baklava
  • JobScheduler quota changes

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#

Curiosity corner#

  • A service can be destroyed and restarted with a null intent. If you return START_STICKY, be ready for intent == null in onStartCommand. Many crashes come from assuming otherwise.
  • bindService can fail without calling onServiceConnected. If the service crashes during creation, you get onBindingDied or onNullBinding, 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#

See running services and their types

adb shell dumpsys activity services com.example.notes

Start a service from the shell

adb shell am startservice -n com.example.notes/.UploadService

Check yourself#

Check yourself

1 / 12

You call `startActivity()` for an activity in another app. Which check does the system perform before delivering the intent?

0 answered · 0 correct

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.

  1. docsForeground services overview | Background workdeveloper.android.comaccessed 2026-10-27↩
  2. docsServices overview | Background workdeveloper.android.comaccessed 2026-10-27↩
  3. docsFeatures and APIs overview | Android 12developer.android.comaccessed 2026-10-27↩
  4. docsProcesses and threads overview | App qualitydeveloper.android.comaccessed 2026-10-27↩
  5. docsAnrdeveloper.android.comaccessed 2026-10-27↩
  6. docsBehavior changes: Apps targeting Android 16 or higherdeveloper.android.comaccessed 2026-10-27↩
  7. docsWorkmanagerdeveloper.android.comaccessed 2026-10-27↩