Required Reading
Plugin Version
"react-native-background-fetch": "4.4.2", "react-native-background-geolocation": "5.3.0",
Mobile operating-system(s)
Device Manufacturer(s) and Model(s)
Iphone 15 Pro Max, Galaxy A16
Device operating-systems(s)
iOS 26.6, Android 16
React Native / Expo version
"react-native": "0.81.5", "expo": "54.0.33", Bare workflow — native projects generated by expo prebuild with a config plugin. New Architecture enabled (RN 0.81 default).
What do you require assistance about?
On both iOS and Android, location updates stop arriving in the middle of an active trip. They correctly engage the stop-timeout while stationary, while getState() still reports enabled: true. Once this happens, movement does not bring updates back: neither motion-activity transitions nor several hundred metres of travel reliably resume delivery. Updates typically return only much later — in our recordings, when the user has been walking again for a sustained period.
Our use case is recording continuous urban travel traces (walking + tram/bus/metro + car), so a multi-minute hole in the middle of a trip is data loss for us, not a battery optimisation. The pattern is especially damaging around public transport: the user walks (tracked fine), waits a few minutes at a stop, boards — and the entire vehicle leg is missing.
We would like help identifying what stops delivery, and what the correct configuration is for "never lose location during an active trip, but still allow the device to park after a long genuine stop" on both platforms.
Recorded evidence
The following is the plugin's own recorded stream (via getLocations()) from an iOS device.
The full JSON is attached. A comparable pattern is observed on Android; we can supply an
Android recording as well if useful.
Trip start — fixes arrive every 7–10 s as expected:
| Time (UTC) |
lat, lon |
accuracy |
speed |
activity |
| 13:38:54 |
42.673805, 23.330248 |
15.1 m |
— |
on_foot 100% |
| 13:40:34 |
42.674315, 23.329867 |
3.5 m |
1.14 |
on_foot 100% |
| 13:41:27 |
42.674887, 23.330046 |
2.5 m |
1.53 |
on_foot 100% |
| 13:42:25 |
42.674543, 23.329105 |
2.1 m |
1.28 |
on_foot 100% |
| 13:42:34 |
42.674509, 23.328986 |
2.1 m |
0.84 |
on_foot 100% |
→ Location delivery stops here, 3 min 39 s after the trip began.
Gap: 13:42:34 → 13:47:58 — 5 min 25 s with zero locations. During the gap the user waited
briefly at a tram stop, boarded, and rode one stop. Straight-line displacement across the gap is
370 m (42.674509,23.328986 → 42.675961,23.333060); the travelled path is longer.
| Time (UTC) |
lat, lon |
accuracy |
speed |
activity |
| 13:47:58 |
42.675961, 23.333060 |
4.9 m |
1.0 |
on_foot 100% |
| 13:48:08 |
42.675927, 23.332914 |
3.8 m |
1.29 |
on_foot 100% |
| 13:48:18 |
42.675866, 23.332804 |
3.2 m |
1.14 |
on_foot 100% |
Delivery resumes only once the user is walking again.
What is puzzling about this
- Delivery stopped 3 min 39 s into an active trip, far short of our
stopTimeout: 15
minutes. We do not understand what, other than stopTimeout, stops delivery this early.
- The motion API reported
on_foot at 100 % confidence throughout — both before and after
the gap. The device was never classified still, even at the moment delivery stopped.
- 370 m of displacement during the gap did not produce a
motionchange. Given the
documented ~200 m stationary-exit behaviour on iOS, we expected the stationary region to be
exited during that leg.
- It happens on both platforms, which rules out the hypothesis (iOS
pausesLocationUpdatesAutomatically) as the whole story — see below.
What we have already tried
- iOS: ran with
pausesLocationUpdatesAutomatically left at its default, then explicitly set to false. The symptom occurs in both cases. (We understand false implicitly sets disableStopDetection: true and disables preventSuspend; we compensate by parking GPS ourselves with changePace(false) after our own 15-minute idle watchdog.)
- iOS: tried
activityType: Other and ActivityType.OtherNavigation. No improvement observed with either.
- Both platforms — app-level resume attempts, neither of which reliably recovers:
- On
onActivityChange with any moving class (on_foot, walking, running,
on_bicycle, in_vehicle) at any confidence, when getState() reports
enabled: true and either isMoving: false or no fix for 90 s → we call
changePace(true).
- On
onHeartbeat, when fixes look stale → getCurrentPosition({samples: 3, timeout: 20}),
compared against a stored "park anchor"; if displacement exceeds a threshold →
changePace(true).
Because the activity-based path fires on any moving class at any confidence, and the
recorded stream shows on_foot @ 100 % throughout the gap, we would have expected it to fire.
It did not produce a resume. From the JS side we cannot distinguish between:
- the
activitychange / heartbeat events not being delivered to JS in this state, or
changePace(true) having no effect while the plugin is in this state.
Listeners registered
onLocation, onMotionChange, onActivityChange, onHeartbeat, onProviderChange,
onConnectivityChange. ready() is called once per JS context; start() is called when the
user enables tracking in-app. We never call stop() except when the user disables tracking.
Questions
- What can stop location delivery mid-trip, well before
stopTimeout elapses, on either
platform? This is our main question — a 3½-minute-old trip being cut with the motion API
reporting on_foot @ 100 % is the part we cannot explain from the docs.
- On iOS with
pausesLocationUpdatesAutomatically: false, our reading of the docs is that
location-services should never be turned off by the plugin. Is that correct, and can
anything override it (desiredAccuracy: Navigation, activityType, Low Power Mode, iOS
suspending the app)? Is TSLocationManager 4.3.x validated against iOS 26?
- On Android, what should we check for this symptom — Doze / battery-optimisation
exemption, OEM background restrictions, foreground-service state? Is there a way to detect
from the plugin that the service was throttled rather than legitimately parked?
- What is the intended mechanism that resumes tracking after the plugin parks, on each
platform? Should changePace(true) from JS always work in that state, and should
onActivityChange continue to be delivered while parked?
- Does
preventSuspend: true / heartbeatInterval: 60 keep the heartbeat firing in this
state? We do not appear to receive heartbeats during the gaps. On iOS, if
preventSuspend is inoperative when pausesLocationUpdatesAutomatically: false, is there
any way to keep a JS watchdog alive?
- Is
stopDetectionDelay expressed in minutes or milliseconds in 5.x? The typedef and the
Config summary table say minutes, but the ActivityConfig example in the docs uses
stopDetectionDelay: 10000 // 10s. We currently pass 15, intending 15 minutes.
- Could
motionTriggerDelay: 30000 be implicated in the Android non-resume? We added it
recently to avoid false trip starts from indoor pottering; we would like to confirm it
cannot suppress a genuine resume.
- Are there known issues with React Native 0.81 / New Architecture where the native side
keeps running but JS stops receiving plugin events?
[Optional] Plugin Code and/or Config
// Called exactly once per JS context. start() is called separately when the
// user enables tracking in the app.
await BackgroundGeolocation.ready({
reset: true,
geolocation: {
desiredAccuracy: BackgroundGeolocation.DesiredAccuracy.Navigation,
stopTimeout: 15, // minutes
showsBackgroundLocationIndicator: false,
stationaryRadius: 25,
pausesLocationUpdatesAutomatically: false, // iOS; symptom occurs with and without this
activityType: BackgroundGeolocation.ActivityType.Other,
},
activity: {
stopDetectionDelay: 15, // minutes (see question 6)
disableMotionActivityUpdates: false,
motionTriggerDelay: 30000, // Android only (see question 7)
},
persistence: {
maxDaysToPersist: 1,
},
http: {
autoSync: false, // we upload trips ourselves
batchSync: false,
},
app: {
stopOnTerminate: false,
preventSuspend: true,
heartbeatInterval: 60,
startOnBoot: true,
enableHeadless: true,
notification: {
title: "CityRhythm",
text: "Enjoy your journey!",
channelName: "CityRhythm",
smallIcon: "drawable/ic_notification",
largeIcon: "drawable/ic_notification",
priority: BackgroundGeolocation.NotificationPriority.Default,
sticky: false,
},
},
logger: {
debug: false, // true in dev builds
logLevel: BackgroundGeolocation.LogLevel.Warning, // Verbose in dev builds
},
});
// NOT set anywhere (left at plugin defaults):
// disableStopDetection, distanceFilter (so 10), disableElasticity,
// elasticityMultiplier, useSignificantChangesOnly, locationAuthorizationRequest,
// activityRecognitionInterval, minimumActivityRecognitionConfidence,
// speedJumpFilter, stopOnStationary, desiredOdometerAccuracy
iOS `Info.plist` (relevant keys):
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>fetch</string>
<string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.transistorsoft.fetch</string>
<string>com.transistorsoft.customtask</string>
</array>
<key>NSMotionUsageDescription</key> <string>…</string>
<key>NSLocationWhenInUseUsageDescription</key> <string>…</string>
<key>NSLocationAlwaysUsageDescription</key> <string>…</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>…</string>
<key>TSLocationManagerLicense</key> <string>…</string>
Android: `tslocationmanagerVersion = "4.3.+"` (via Gradle ext vars), license key supplied.
Location permission granted as **Always** / **Allow all the time**; Motion & Fitness (iOS) and
Physical Activity (Android) permissions granted.
[Optional] Relevant log output
Required Reading
Plugin Version
"react-native-background-fetch": "4.4.2", "react-native-background-geolocation": "5.3.0",
Mobile operating-system(s)
Device Manufacturer(s) and Model(s)
Iphone 15 Pro Max, Galaxy A16
Device operating-systems(s)
iOS 26.6, Android 16
React Native / Expo version
"react-native": "0.81.5", "expo": "54.0.33", Bare workflow — native projects generated by
expo prebuildwith a config plugin. New Architecture enabled (RN 0.81 default).What do you require assistance about?
On both iOS and Android, location updates stop arriving in the middle of an active trip. They correctly engage the stop-timeout while stationary, while
getState()still reportsenabled: true. Once this happens, movement does not bring updates back: neither motion-activity transitions nor several hundred metres of travel reliably resume delivery. Updates typically return only much later — in our recordings, when the user has been walking again for a sustained period.Our use case is recording continuous urban travel traces (walking + tram/bus/metro + car), so a multi-minute hole in the middle of a trip is data loss for us, not a battery optimisation. The pattern is especially damaging around public transport: the user walks (tracked fine), waits a few minutes at a stop, boards — and the entire vehicle leg is missing.
We would like help identifying what stops delivery, and what the correct configuration is for "never lose location during an active trip, but still allow the device to park after a long genuine stop" on both platforms.
Recorded evidence
The following is the plugin's own recorded stream (via
getLocations()) from an iOS device.The full JSON is attached. A comparable pattern is observed on Android; we can supply an
Android recording as well if useful.
Trip start — fixes arrive every 7–10 s as expected:
→ Location delivery stops here, 3 min 39 s after the trip began.
Gap: 13:42:34 → 13:47:58 — 5 min 25 s with zero locations. During the gap the user waited
briefly at a tram stop, boarded, and rode one stop. Straight-line displacement across the gap is
370 m (42.674509,23.328986 → 42.675961,23.333060); the travelled path is longer.
Delivery resumes only once the user is walking again.
What is puzzling about this
stopTimeout: 15minutes. We do not understand what, other than
stopTimeout, stops delivery this early.on_footat 100 % confidence throughout — both before and afterthe gap. The device was never classified
still, even at the moment delivery stopped.motionchange. Given thedocumented ~200 m stationary-exit behaviour on iOS, we expected the stationary region to be
exited during that leg.
pausesLocationUpdatesAutomatically) as the whole story — see below.What we have already tried
pausesLocationUpdatesAutomaticallyleft at its default, then explicitly set tofalse. The symptom occurs in both cases. (We understandfalseimplicitly setsdisableStopDetection: trueand disablespreventSuspend; we compensate by parking GPS ourselves withchangePace(false)after our own 15-minute idle watchdog.)activityType: OtherandActivityType.OtherNavigation. No improvement observed with either.onActivityChangewith any moving class (on_foot,walking,running,on_bicycle,in_vehicle) at any confidence, whengetState()reportsenabled: trueand eitherisMoving: falseor no fix for 90 s → we callchangePace(true).onHeartbeat, when fixes look stale →getCurrentPosition({samples: 3, timeout: 20}),compared against a stored "park anchor"; if displacement exceeds a threshold →
changePace(true).Because the activity-based path fires on any moving class at any confidence, and the
recorded stream shows
on_foot @ 100 %throughout the gap, we would have expected it to fire.It did not produce a resume. From the JS side we cannot distinguish between:
activitychange/heartbeatevents not being delivered to JS in this state, orchangePace(true)having no effect while the plugin is in this state.Listeners registered
onLocation,onMotionChange,onActivityChange,onHeartbeat,onProviderChange,onConnectivityChange.ready()is called once per JS context;start()is called when theuser enables tracking in-app. We never call
stop()except when the user disables tracking.Questions
stopTimeoutelapses, on eitherplatform? This is our main question — a 3½-minute-old trip being cut with the motion API
reporting
on_foot @ 100 %is the part we cannot explain from the docs.pausesLocationUpdatesAutomatically: false, our reading of the docs is thatlocation-services should never be turned off by the plugin. Is that correct, and can
anything override it (
desiredAccuracy: Navigation,activityType, Low Power Mode, iOSsuspending the app)? Is TSLocationManager 4.3.x validated against iOS 26?
exemption, OEM background restrictions, foreground-service state? Is there a way to detect
from the plugin that the service was throttled rather than legitimately parked?
platform? Should
changePace(true)from JS always work in that state, and shouldonActivityChangecontinue to be delivered while parked?preventSuspend: true/heartbeatInterval: 60keep the heartbeat firing in thisstate? We do not appear to receive heartbeats during the gaps. On iOS, if
preventSuspendis inoperative whenpausesLocationUpdatesAutomatically: false, is thereany way to keep a JS watchdog alive?
stopDetectionDelayexpressed in minutes or milliseconds in 5.x? The typedef and theConfig summary table say minutes, but the
ActivityConfigexample in the docs usesstopDetectionDelay: 10000 // 10s. We currently pass15, intending 15 minutes.motionTriggerDelay: 30000be implicated in the Android non-resume? We added itrecently to avoid false trip starts from indoor pottering; we would like to confirm it
cannot suppress a genuine resume.
keeps running but JS stops receiving plugin events?
[Optional] Plugin Code and/or Config
[Optional] Relevant log output