← Back to blog

Background Location on Real Phones: Why It Works in the Demo and Fails in Production

In the demo, driver location tracking runs without issues. In production — phone in a pocket, app in the background all day — the OS kills the app and the location disappears. How to handle it properly.

In the demo it works perfectly. GPS coordinates arrive every 3 seconds, the driver moves smoothly on the map, the dispatcher sees an exact position. The demo runs for 20 minutes, the phone sits on the table screen-up, the app is in the foreground.

In production it is different. A shift lasts 8 hours. The phone is in a pocket or on a mount in the car, screen off, app in the background. After 45 minutes the location stops coming. After 2 hours the dispatcher last sees the driver in a car park outside a supermarket.

This is not a bug in the code. It is the result of how modern mobile operating systems manage battery — and how OEM manufacturers pile their own layer on top.

Why the Demo and Production Are Not the Same

When the app runs in the foreground, the OS gives it full priority. The GPS chip sends updates, the CPU does not sleep, network calls go through immediately. This is the demo state.

The moment the user presses the home button or turns off the screen, the app moves to the background. The OS starts saving battery. Concretely that means:

  • Android Doze mode (since Android 6): after a few minutes without movement or interaction, the OS suspends network access and defers alarms. GPS receives, but packets do not reach the server.
  • Background App Refresh throttling (iOS): an app in the background gets CPU time only occasionally, for minutes at a time. In between, nothing runs.
  • Foreground service on Android partly solves the problem — but only partly. It keeps the process alive, but the OEM battery manager can still kill it.

Doze mode and foreground service are standard Android. On top of that, each manufacturer adds their own layer.

Per-OEM Reality: Samsung, Xiaomi, Huawei, OnePlus

This is where it gets unpleasant. Every major Android manufacturer has their own battery management system that works differently and requires a different permission flow.

Samsung (One UI) has "Adaptive battery" and "App power management". The app must be explicitly excluded from "Sleeping apps" — either by the user manually, or via the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent. But Samsung added "Background app limits" in newer One UI versions, which operates independently. You need to handle both.

Xiaomi (MIUI) is notoriously aggressive. MIUI has "Battery saver" and also "Autostart" permission, without which the app will never launch a foreground service after a phone restart — even if the OS has not gone to sleep. Granting ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS is not enough. The app must open the MIUI deep link to its own Battery Settings page and guide the user step by step.

Huawei (EMUI / HarmonyOS) has a "Protected apps" list and "App launch" settings. Without being added to Protected apps, EMUI will stop a foreground service within 5–10 minutes of the screen turning off. On older EMUI versions (8, 9) this did not exist; on newer HarmonyOS the situation is different again.

OnePlus (OxygenOS) in older versions notoriously ignored foreground service notifications and killed the app. OxygenOS 14+ improved this, but the "Battery optimization" dialog is not enough — you need to direct the user to "Advanced optimization" in Battery settings.

This is per-OEM work. You cannot write one piece of code and assume it works the same on Samsung and Xiaomi.

What Happens When the OS Kills the App

A foreground service has one key flag: START_STICKY. It tells the OS to restart the service after it is killed. But the restart takes seconds to tens of seconds. In the meantime, GPS coordinates arrived that no one recorded.

Worse case: the OS kills the app and does not restart it — due to memory pressure, or because MIUI Autostart does not have permission. In that case the service does not start at all until the user manually opens the app.

How to detect this? Heartbeat. The app sends a ping to the server every N seconds. If no ping arrives for longer than 2× the interval, the server knows the app is dead — not offline. That information has a different business impact than "driver has no signal".

On the app side: at startup we check the timestamp of the last successful GPS coordinate. If it is older than 5 minutes and the app just started, we know this was a restart after a kill. Log it, report it to the monitoring system.

Offline Queue and Recovery

GPS coordinates that arrive during periods without connectivity cannot be discarded. The dispatcher needs to know where the driver went — even through a tunnel with no signal.

Solution: a local queue of coordinates. Each coordinate is saved to local storage (SQLite, Hive, SharedPreferences — depending on platform). A separate worker thread periodically tries to send the coordinates to the server. On success it deletes them. On failure it leaves them and retries with exponential backoff.

The queue must have an upper limit — you cannot accumulate coordinates indefinitely. A reasonable limit is 2–4 hours of records at normal frequency. Older records are discarded, but the fact that data for that period is missing is recorded.

Recovery after restart: when the app starts, the worker checks whether the queue contains unsent records and sends them in batches. The server must handle receiving out-of-order coordinates and idempotent writes (the same coordinate sent twice must not create a duplicate).

Online/Offline Semantics and Heartbeat

"Driver is offline" is an ambiguous state. It can mean:

  1. The driver intentionally turned off availability (break, end of shift)
  2. The app lost GPS signal (tunnel, underground car park)
  3. The app lost network connectivity
  4. The OS killed the app in the background
  5. The phone ran out of battery

For a dispatch system these are different situations requiring different actions. A heartbeat system distinguishes between them:

  • GPS pause without a heartbeat gap = signal missing, but the app is running
  • Heartbeat missing, but the last message was "going offline" = intentional disconnection
  • Heartbeat missing without any message = app dead or battery flat

The heartbeat interval must be a compromise. Every 30 seconds is too frequent — it drains battery and overloads the server with 100 connected drivers. Every 5 minutes is too sparse — you do not know for 5 minutes whether the app is alive. A reasonable compromise is 60–90 seconds, with the server marking the driver as "suspect offline" after 3 missed heartbeats (2.5–4.5 minutes of latency).

Permission Flow: Design It Upfront, Not as an Afterthought

The permission flow for background location must be designed as part of onboarding, not added later. The user must understand why the app needs location "always" (not just while using the app), otherwise they will deny it in the system dialog.

On Android that means a progressive request: first "While using", then an explanation of why "Always", then a request for "Always" via settings (the system dialog does not allow directly granting "Always" for background — the user must go to Settings). Google designed this flow intentionally as friction. You cannot bypass it, only explain it well.

On iOS: "When In Use" → CoreLocation usage description → request upgrade to "Always" → description of why. iOS 14+ added a "Precise location" / "Approximate location" toggle — driver location tracking needs Precise, and the user must consciously approve it.

After "Always" is granted, do not forget to check again on every launch. The user can revoke permission at any time in Settings — without notifying the app.

Telemetry: Without Data You Do Not Have a Problem, You Have Complaints

An app without telemetry gives you only driver reports: "location wasn't working." You do not know when, where, which phone, which Android version, whether it was Doze, a MIUI kill, or a network outage.

Minimum telemetry for background location:

  • Number of GPS updates per hour (anomaly = Doze or kill)
  • Number of heartbeat failures and their duration
  • Restart events flagged as "after kill" vs. "intentional start"
  • OS version and phone model on every event

With this data you can see: Xiaomi MIUI 14 accounts for 60% of kill events even though it is 20% of the fleet. Samsung One UI 6 has a problem after 23:00 when it sleeps more aggressively. These are real numbers we saw on Carivio and TaxiLight.

Without telemetry the problem exists, but you have no leverage to prioritise it or prove that a fix worked.

An Honest Summary

Background location in a dispatch app is not a one-size-fits-all problem. It is a sum of: OS battery management + per-OEM layer + permission flow + offline queue + heartbeat + kill detection + telemetry.

The demo works because it eliminates most of that. The app is in the foreground, phone on the table, session runs 20 minutes. Production cannot eliminate any of it.

This cannot be solved with a single setting. It is engineering work that must be designed upfront — not patched after driver complaints.

If you are building a dispatch system or a driver app and do not want to discover problems only in production, get in touch — we will walk through the architecture and tell you where the gaps are.

FAQ

Is background location more reliable on iOS than on Android?

On iOS the situation is more consistent — Apple gives CLLocationManager relatively predictable behaviour and there is no ecosystem of OEM battery optimisations like on Android. But iOS is not immune either: when switching to background mode you get limited CPU time, the "always" permission requires more approval steps from the user, and Low Power Mode throttles location. Android has more problems, but they are at least documented and solvable per-OEM.

Is setting up a foreground service enough?

A foreground service is a necessary condition, not a sufficient one. It prevents Android from killing the process under normal memory pressure. But it does not protect you from a user hitting "Force stop" manually, from the MIUI aggressive battery manager, or from Doze mode if you do not have the right wake lock configuration. Foreground service is the foundation — on top of it you need kill detection, an offline queue, and a heartbeat.

Do I need to implement per-OEM handling for each manufacturer separately?

Yes, if you want production reliability. You cannot write one piece of code and assume it works the same everywhere. In practice that means: detecting the manufacturer at runtime, directing the user to the correct system settings via deep link (dontkillmyapp.com documents URL schemes for MIUI, EMUI, OxygenOS), and testing on physical devices from each manufacturer — not just on an emulator. Emulators do not simulate battery optimisations.

Facing a similar problem? Get in touch.

Book a consultation