Boot & killed-app behavior
BGeo’s contract is that tracking and upload are native, and JS is
optional at runtime. Once ready()
has started tracking, the foreground service (Android) or location engine
(iOS) keeps recording, filtering, persisting to SQLite, and uploading with no
JS context required. Your onLocation/onMotionChange/etc. listeners are UI
observers on top of that pipeline, not part of it — see the
Tracking lifecycle concept
for the one-paragraph summary this guide expands into a full matrix.
“Killed” isn’t one state. This guide distinguishes five, because each has a different platform story:
- Backgrounded — the app is merely not in the foreground. Both platforms keep the JS context alive; nothing here is really about killing at all.
- Swiped away / task removed — the user removes the app from the Android recent-apps list. iOS has no equivalent gesture that’s distinct from a force-quit.
- Force-quit (iOS meaning) — the user swipes the app away in iOS’s app-switcher, which is Apple’s explicit signal to stop the process and (on older iOS behavior) suppress most background relaunch triggers.
- OS-killed (memory eviction) — the OS terminates the process itself to reclaim memory, with no user action. The most common real-world case is an overnight eviction of a backgrounded app.
- Device reboot — the whole OS restarts; every process is gone and every
in-memory registration is gone with it, though some platform registrations
(Android’s boot broadcast, iOS’s
locationdstate) persist across the reboot itself.
The matrix
The verified end-state (device-tested, see the sourcing note in each row) for what keeps tracking alive, what relaunches the process, and the expected tracking gap:
| Scenario | Android | iOS |
|---|---|---|
| App backgrounded | Foreground service keeps running; JS context alive. No gap. | Session engine (useSessionEngine, default on iOS 17+) keeps a live location stream that auto-pauses at the parked keep-alive and auto-resumes on movement — no relaunch needed, JS context alive. No gap. |
| Swiped away (task removed) | onTaskRemoved in the foreground service immediately restarts it (START_STICKY) — tracking continues without ever fully stopping. No gap. | No distinct gesture — the app-switcher swipe is iOS’s force-quit; see the next row. |
| Force-quit (iOS meaning) | N/A — Android has no force-quit concept distinct from task removal, handled above. | The session engine relaunches the process and recreates its session synchronously at launch (recreateSessionStreamOnRelaunch + CLServiceSession, iOS 18+), continuing the authorization grant held before the kill. Device-verified across three force-quit deaths: relaunch in ~1 second, roughly a second of tracking loss. The rolling wake region (armed in both motion states, re-centered on enterMoving) is a secondary relaunch trigger and the only one on the pre-iOS-17 legacy path, which has no session to recreate — that path relaunches on a region exit, roughly 1–2× stationaryRadius of silence. |
| OS-killed (memory eviction) | Foreground service is designed not to be evicted under normal conditions; if the process still dies, onTaskRemoved/service restart semantics apply the same as above. | Same session-recreation-at-relaunch path as force-quit — an OS-initiated termination is not distinguished from a user force-quit by the engine. The overnight-eviction case (backgrounded app, process dies while the phone sleeps) is closed by the same mechanism: the session is recreated synchronously in init, and liveUpdates auto-resume relaunches the app in the background the moment movement resumes, with no foreground visit required. |
| Device reboot | BootReceiver restarts the service on BOOT_COMPLETED/LOCKED_BOOT_COMPLETED when tracking was enabled and startOnBoot is true. With false, nothing survives the reboot (geofences, activity-recognition PendingIntents, the foreground service all die with the process) until the app is next opened. | No boot broadcast exists, so startOnBoot:true is best-effort by design: the wake-region and significant-location-change registrations persist in locationd across the reboot and relaunch the app in the background once movement or a region event triggers them — device-verified relaunch in roughly 1 minute after boot, parked, no movement required, with the wake region confirmed still armed in locationd post-reboot. false is honoured via kern.boottime reboot detection: init compares the persisted boot time against the current one (tolerant of clock drift, not systemUptime, which pauses during device sleep) and drops the persisted enabled flag before any auto-resume path runs, so relaunch finds tracking disabled and does nothing. |
stopOnTerminate & startOnBoot
These two config keys govern the two sides of app-kill behavior — whether tracking survives a kill, and whether it survives a reboot — and both are honoured on both platforms, but through different mechanisms because iOS has no reliable terminate or boot hook.
stopOnTerminate
(default false): whether tracking should stop when the app process is
killed.
- Android tears tracking down directly, synchronously, in the
foreground service’s
onTaskRemoved. - iOS has no terminate hook to run code in, so the flag is honoured at
next relaunch instead:
initdrops the persistedenabledflag before any auto-resume path runs (bothrecreateSessionStreamOnRelaunchand the deferredstartTrackingare gated on it). JS seesstate.enabled === falseon the nextready()/app open and has to callstart()again — there’s a short window between the actual kill and the next relaunch where, from iOS’s perspective, tracking hasn’t stopped yet (the process is simply gone), but no further fixes are produced because the process isn’t running either way.
startOnBoot (default
false): whether tracking should resume automatically after a reboot, if it
was enabled when the device went down.
- Android’s implementation matches its name literally:
BootReceiverrestarts the service onBOOT_COMPLETED/LOCKED_BOOT_COMPLETEDwhen bothisEnabled()and this flag are true. - iOS has no boot broadcast, so
trueis best-effort: the wake-region and SLC registrations that survive the reboot inlocationdare what relaunch the app, not a dedicated boot handler.falseis still honoured — reboot detection viakern.boottimedrops the persistedenabledflag on the first relaunch after a reboot, so nothing auto-resumes.
Android headless tasks
While the JS context isn’t running (app killed, or not yet launched since
boot), Android can still invoke a small piece of JS through
registerHeadlessTask():
the OS spins up a HeadlessJsTaskService, gives your task a short execution
window, and tears the JS context back down when it returns.
Register it outside any component — in index.js, alongside your
AppRegistry.registerComponent() call, so it’s wired up before any screen
ever mounts:
import { AppRegistry } from 'react-native';import BackgroundGeolocation from '@dc-bgeo/react-native-background-geolocation';import App from './App';
BackgroundGeolocation.registerHeadlessTask(async (event) => { switch (event.name) { case 'heartbeat': console.log('[headless] heartbeat', event.location); break; case 'motionchange': console.log('[headless] motionchange', event.isMoving); break; case 'geofence': console.log('[headless] geofence', event.identifier, event.action); break; default: console.log('[headless] event', event.name, event); }});
AppRegistry.registerComponent('YourAppName', () => App);Event shape. A headless
HeadlessEvent has no params
wrapper — the event’s payload fields are flattened directly alongside a
top-level name:
interface HeadlessEvent { name: 'heartbeat' | 'motionchange' | 'geofence' | 'providerchange' | 'powersavechange' | 'http' | 'connectivitychange'; [key: string]: any; // payload fields, flattened}Runtime event set. Seven event types reach the headless task:
heartbeat, motionchange, geofence, providerchange,
powersavechange, http, and connectivitychange. location is
deliberately excluded — forwarding every accepted fix headlessly would spin
up HeadlessJsTaskService (and a wakelock) per fix while tracking, draining
the battery and risking ANRs/foreground-service-start restrictions. Fixes
taken while the JS context isn’t running are still durably queued and
uploaded by the native HTTP store; they just don’t reach a headless
listener. HeadlessEvent['name'] lists exactly these seven names, so
narrowing on event.name is exhaustive. See the per-event Headless line on the
Events reference for which events carry
which fields headlessly.
Constraints. A headless task runs on a strict execution budget: keep it
fast, do no UI work (there’s no mounted component to update), and prefer
await-able async work — anything long-running risks the OS killing the
task before it completes.
iOS: no headless
iOS has no AppRegistry-driven headless task service, so
registerHeadlessTask()
retains your task on iOS but never invokes it while the app process isn’t
running — JS simply does not run for a killed iOS app, headless or
otherwise. This is by design, not a gap to work around: the native engine
already keeps recording, filtering, persisting, and uploading independently
of JS, so nothing at the OS layer is actually waiting on JS to run.
The design answer for “I need this to survive a kill” is the same on both platforms — do it natively, not in JS:
- Native uploads (queue drain, retry, poison-record handling, and
authorizationtoken refresh) all continue with no JS context, on both platforms. See Authorization (JWT refresh). - Attach anything the server needs at ingest time via
urlrequest shaping (params,extras) rather than deriving it in a JS listener. - Use
logUrlfor native-side diagnostic logging that needs to reach your server even when nothing JS ever runs.