Mobile Computing · Background Execution
Declaring Long-Running Tasks Without Being Killed
How an app asks for continued execution after it leaves the screen, what each declaration actually grants, and why the grant is audited rather than trusted.
There is a class of work that cannot be deferred. A call is being recorded, a navigation session is tracking a route, a file is being written to removable storage, a device is being paired over a protocol that requires a live connection. Deferring any of these to a maintenance window destroys the thing the user asked for.
Both platforms provide a way to say so. The mechanism is a declaration: the app states what it is doing, the system decides whether that statement is credible, and the grant that follows is bounded and audited. Understanding the audit is what separates a declaration that survives an OS update from one that gets the app killed.
A declaration is a claim about user intent
The Android foreground service is the clearest form of the pattern. The app starts a service, calls a method that promotes it to foreground, and supplies an ongoing notification. The promotion raises the process’s rank above cached and keeps it out of the freezer; the notification is the price, because the user is now told that the app is doing something on their behalf and can stop it.
The declaration has three parts, and all three are checked.
The service must declare a type in the manifest, and the type must be one the platform recognises: media playback, location, camera, microphone, connected device, data sync, and so on. The type determines which permissions the service needs and which runtime restrictions apply to it. A service that declares dataSync is telling the platform that it is moving data on the user’s behalf, and the platform will apply the quota associated with that type.
The start must be permitted from the app’s current state. Since the background execution limits introduced with Android 8.0, an app in the background cannot generally start a service; it can start a foreground service only if an exemption applies, such as the app having a visible activity, receiving a high-priority message, or handling a broadcast the system considers exempt. On later versions the check became stricter still: starting a foreground service from the background throws unless the app qualifies for an exemption, and the exemption list is short and specific.
The notification must be posted within a short window of the promotion. A foreground service that does not display its notification is not a foreground service; the platform treats the missing notification as a failure of the declaration, and the process reverts to a lower rank or is stopped.
What the grant does not include
A foreground service keeps the process running. It does not grant unlimited network access, unlimited CPU, or immunity from the low-memory killer’s preference ordering — a foreground service process ranks above a cached one but below a process hosting a visible activity, and under extreme pressure it can still be killed.
Nor does it grant indefinite duration for every type. The platform associates time limits with particular service types, and a service that exceeds the limit for its type is subject to being stopped. The documented behaviour is that the system notifies the service before the limit is reached, giving it an opportunity to wind down; a service that ignores the notice is stopped by the platform rather than by the app. The limits themselves have changed across releases and differ by type, which is the reason a design that depends on a specific number of minutes is fragile.
The deeper constraint is that the type is a contract about behaviour, not a label that unlocks resources. A service declared as location tracking while it is in fact uploading logs is a misdeclaration, and the platform’s enforcement — through review, through type-specific permission checks, and through the notification’s visibility to the user — is designed to catch it. The honest reading is that the type describes what the app is doing, and the app must pick the one that is true.
// Promotion is a two-step operation: start, then declare.
// If the notification is not posted promptly, the platform
// demotes the process and the declaration is void.
val notification = buildOngoingNotification("Recording")
startForeground(SERVICE_ID, notification, FOREGROUND_SERVICE_TYPE_MICROPHONE)
// The grant is time-limited for this type; the callback is the
// only reliable signal that the limit is near.
override fun onTimeout(startId: Int, fgsType: Int) {
stopRecordingAndPersistState()
stopSelf(startId)
}
The onTimeout callback is the load-bearing part of that snippet. It exists because the platform will not wait for the app to notice the limit by itself.
The iOS assertion and its expiration handler
Apple’s model is a request rather than a declaration. When the app moves to the background, it may ask for additional execution time; the system grants an extension and calls an expiration handler shortly before the extension ends. The app is expected to finish its work and call the completion handler, and an app that has not done so by the time the handler returns is terminated.
The extension’s length is not published and has varied across releases, so the expiration handler is the only contract. The correct structure is to treat the handler as an interrupt: stop starting new work, persist what is in flight, release what must be released, and signal completion.
The set of activities that legitimately need the extension is narrow and is enumerated in the platform’s documentation: finishing a file write, completing a user-initiated action, and similar. Background execution that is not tied to an in-flight user action belongs to a different mechanism — a background task request, a background session, or a notification — and using the assertion for periodic work is the pattern the platform is designed to prevent.
Why the audit exists
The design constraint behind all of this is that the phone has one battery and many apps that would each like to run continuously. Without an audit, the cheapest strategy for any single app is to declare a foreground service permanently and let the user’s battery pay for it. The audit changes the economics: a declaration costs a visible notification, a permission, a type, and an accountability to the platform’s policy, so only the apps whose function genuinely requires continuous execution will pay.
The alternative designs are instructive. An earlier Android model allowed background services with no notification and no type, and the result was a large population of processes that were nominally background but effectively always running. The current model trades developer convenience for a ranking that reflects what the user can see. On iOS the same trade appears as the absence of a general “keep running” API: an app cannot ask to run indefinitely, only for a specific documented purpose.
When the declaration is rejected
The most common failure is a start from the wrong state. An app that receives a broadcast in the background and starts a foreground service throws an exception on modern Android unless the broadcast is one of the exempt kinds. The exception arrives at the call site, and the correct response is to fall back to a deferred mechanism rather than to retry.
The second is a missing permission. Each service type maps to a permission — microphone access for a microphone service, location for a location service — and the permission must be held at the moment of promotion, not merely declared in the manifest. A user who revokes a permission while the service runs causes the service to lose its type and therefore its grant.
The third is a notification that the user dismisses. On some versions, dismissing the notification of a foreground service stops the service. An app that treats the notification as decoration discovers that the user has a stop button.
The fourth is the interaction with battery optimisation settings. Devices from some manufacturers apply additional restrictions on top of the platform’s, and an app that is exempt from the platform’s optimisation is not necessarily exempt from the vendor’s. The documented behaviour is what the platform guarantees; anything beyond it is device-specific and should be treated as unavailable rather than assumed.
The fifth is the restart path. A service that is killed and restarted by the system after a reboot receives a null intent on some versions, and code that dereferences the intent’s extras crashes at the moment it is most needed. The service must be able to reconstruct its work from its own persisted state, which is the durable record again — see Durable Deferred Work and the Persistence Boundary.
Choosing between a declaration and a deferral
The decision rule is about whether the work has a deadline the user can perceive. A recording has one: every second not recorded is lost. A sync does not: it can happen in five minutes or in five hours, and the user will not notice the difference unless the app’s own interface claims otherwise.
Work that is genuinely continuous and user-visible gets a declaration, with the notification that the declaration requires. Work that is periodic and not user-visible gets deferred, with constraints that describe the conditions under which it can run. Work that is neither should not run in the background at all.
The mixed case is the one that causes trouble: a session that begins as user-visible and continues after the user has moved on. A navigation session that the user has left running in the background is still user-intended, and it declares a location type. A download that the user started and then abandoned is not, and it belongs in a system-owned transfer, as described in Handing a Transfer to a System Daemon So It Outlives the App. Deciding which case applies at the moment the user leaves is the design decision that the platform cannot make for the app.
Releasing the grant
A grant that is never released is a leak of the user’s battery and of the app’s standing. The service should stop as soon as its work is done, and the notification should disappear with it. A service that stays in the foreground after its work has finished, because stopping it would require an extra code path, is the exact behaviour the audit exists to discourage — and on devices that surface battery usage by app, it is visible.
References
- Android Developers — Foreground services — Android Developers
- Android Developers — Background execution limits — Android Developers
- Apple Developer Documentation — Extending your app's background execution time — Apple Inc.
- Android Developers — Services overview — Android Developers