If you have scoped a mobile app that tracks location in the background, you have probably been given an estimate based on the API surface. expo-location has a startLocationUpdatesAsync. It takes a few lines. The demo works on the first afternoon.
The demo is not the feature. The feature is everything that happens after the operating system decides your app is not important right now.
This is written from building MileageSync, an automatic mileage tracker where a missed drive is a missed tax deduction. That constraint — a silent failure costs the user real money months later, when it is far too late to fix — forced a set of decisions worth describing.
Why this is harder than it looks
A foreground app has the OS’s attention. A background app is a guest. iOS and Android both aggressively reclaim resources from guests, and they are not obliged to tell you.
The result is a specific and unpleasant failure signature: your app does not crash, it just stops working. No exception, no error report, no bad review explaining what happened. The user finds out weeks later when the data they were relying on is not there.
That shape — silent, delayed, discovered by the user — is what makes background work different from ordinary feature work. Ordinary bugs announce themselves.
Four failure modes worth designing for
1. The process dies and takes your state with it
Both platforms will terminate your app. On Android this is routine. When the OS later restarts your background task, it starts a fresh JavaScript runtime — your in-memory state is gone. Anything that was mid-trip is now nothing at all.
The fix is not clever: persist the state machine, not just the results. MileageSync writes engine state to on-device SQLite as it changes, so a restart resumes an in-progress trip rather than discarding it. Storing only completed trips would lose exactly the drive that was happening when the OS intervened.
2. Battery optimisation throttles you into uselessness
Android’s battery optimisation can reduce your update frequency to something that no longer resembles tracking. The app is running. It is receiving location updates. They are just far too sparse to reconstruct a route.
Two things help. First, use a foreground service where the platform expects one — Android 12 and later restrict when these can start, so profile switching has to be deferred rather than attempted at an arbitrary moment. Second, and more important: measure whether tracking is actually healthy, rather than assuming it is because you called the start function.
3. Your storage handle goes stale
A less obvious one. After a long background period, a SQLite handle opened in a previous runtime may no longer be valid. Writes fail. If the failure path is not handled, the trip is captured and then thrown away at the last step, which is the worst possible place to lose it.
Detect the dead handle, reopen, and retry. This is unglamorous and it is the difference between a tracker that works and one that appears to.
4. GPS noise invents trips that never happened
A phone sitting on a desk produces GPS drift. Naively, drift looks like slow movement, and slow movement looks like a trip. Meanwhile a real drive that stops at three red lights looks like three separate trips if your idle threshold is too aggressive.
Both errors are visible to the user, and both destroy trust — a log full of phantom two-minute drives is worse than an empty one, because now everything in it is suspect.
The technique that made this tractable
None of the above is testable against real hardware in any reasonable loop. You cannot drive around for an hour to check a threshold change.
So the detection logic was written as pure functions over location samples. Given an array of points, decide whether a trip started, continued, or ended. No device APIs, no I/O, no clock — just data in, decision out.
That single structural decision made roughly a hundred automated tests possible against scenarios that would otherwise require a car:
- a runtime restart mid-trip, simulated by replaying persisted state
- GPS drift on a stationary phone, which must not start a trip
- stop-and-go traffic, which must not split one drive into five
- a trip that ends while the app is terminated
The least testable part of the app became the best tested part, purely by moving the decision out of the place where the device lives.
Show the user the tracking is alive
The last piece is a product decision, not a technical one. Because silent failure is the core risk, MileageSync computes a live tracking-health score from permissions, background task registration, battery optimisation state, and GPS heartbeat — and surfaces it, with one-tap fixes.
There is also a diagnostics console exposing live trip state and filterable event logs. It is genuinely useful to users, and it is the first place we look when a report comes in — the alternative is guessing about a device we do not have.
If you are scoping one of these
Ask where the recovery paths are. The happy path is a day’s work. Everything above is the rest of the project, and it is not optional — an app that tracks correctly 95% of the time is not 95% of a product, because the user cannot tell which 5% is missing.
If you are building something in this territory, our mobile app development work covers exactly this, and the MileageSync case study documents the implementation in more detail.