Skip to content

[Bug]: getCurrentPosition returns a hours-old cached location despite maximumAge: 0 (iOS) - plugin 5.5.0 #1711

Description

@gilles-f

Required Reading

  • Confirmed

Plugin Version

5.5.0

Flutter Doctor

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.44.7, on Microsoft Windows [version 10.0.26200.9168], locale fr-CH)
[√] Windows Version (Windows 11 or higher, 25H2, 2009)
[√] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2026 18.9.1)
[√] Connected device (3 available)
[√] Network resources

• No issues found!

Mobile operating-system(s)

  • iOS
  • Android

Device Manufacturer(s) and Model(s)

iPhone 17 Pro

Device operating-systems(s)

iOS 26.6

What happened?

Environment

  • Plugin: flutter_background_geolocation 5.5.0 (Flutter)
  • Platform: iOS (iPhone 17 Pro, iOS 26.6)
  • The call:
    bg.BackgroundGeolocation.getCurrentPosition(
      samples: 1,
      timeout: 30,
      persist: false,
      // maximumAge not passed → default 0
    );

Summary

During a day-long recorded activity, getCurrentPosition repeatedly returned a
location almost 3 hours old and ~130 km away (the morning departure point),
even though:

  1. maximumAge was 0 (the documented default, we expected "no cached fix
    accepted", but the plugin appears to treat 0 as "no age constraint"), and
  2. a fresh fix had been delivered 29 seconds before the call (and 200+
    location updates had been received since the stale fix was recorded).

The stale location is served via the request-service cache path, the native
log explicitly shows [CACHED] getCurrentPosition satisfied from lastLocation.

Relation to issue #1691

This is the same symptom family as
#1691
(iOS getCurrentPosition returning locations older than maximumAge since
v5.x), which was auto-closed as stale. #1691's discussion attributed the stale
result to GPS acquisition delay ("it takes time to connect to satellites /
there is no GPS indoors"). Our log rules that explanation out: a fresh
outdoor fix (±3.82 m, age: 79 ms) was delivered 29 seconds before the
getCurrentPosition call, yet the plugin still served a fix recorded 2 h 52
earlier and 130 km away
. The SDK wasn't waiting for GPS, its request-service
lastLocation was simply frozen on an old-but-very-accurate sample (see
Analysis below), which also explains why samples: 2 only mitigated #1691
instead of fixing it.

Timeline (from the TSLocationManager native log, 22 Aug 2026, times CEST)

08:18:52 — motionchange at the day's departure point ("home", coords
redacted below as 46.25x/6.07x). This fix — ±2.00 m accuracy — is the one
that will be resurrected all day.

08:18 → 11:10 — a ~2 h drive + the start of a hike are tracked normally:
200+ didUpdateLocations deliveries, e.g. still flowing 29 s before the
problematic call:

2026-08-22 11:10:19.186 📍<+46.69561031,+7.86718045> +/- 3.82m (speed 0.58 mps / course 281.31) @ 22.08.26 11.10:19 | age: 79 ms

11:10:48 — the app calls getCurrentPosition (samples: 1, timeout: 30,
persist: false). The device is outdoors, on a mountain trail with clear sky
view
(the fresh ±3.82 m fix 29 s earlier proves GPS reception). The plugin
serves the 08:18:52 fix — 2 h 52 old, ~130 km from the device's actual
position:

2026-08-22 11:10:48.478 🔵 -[TSTrackingService getCurrentPosition:]
2026-08-22 11:10:48.478 🟢 -[TSLocationRequestService requestLocation:] [getCurrentPosition] maximumAge: 0
2026-08-22 11:10:48.478
╔═══════════════════════════════════════════════════════════
║ -[TSSingleLocationRequest trySatisfyWithLocation:now:] 📍🔎 [getCurrentPosition] desiredAccuracy: 5.0 m, maximumAge: 0 ms
╚═══════════════════════════════════════════════════════════
2026-08-22 11:10:48.479 🔵 +[TSLocationHelper pickBestLocationBetween:and:desiredAccuracy:] desiredAccuracy: 5.000000
- A: 📍<+46.25xxxxxx,+6.07xxxxxx> +/- 2.00m (speed 7.62 mps / course 354.34) @ 22.08.26 08.18:52
- B: 📍<+46.25xxxxxx,+6.07xxxxxx> +/- 2.00m (speed 7.62 mps / course 354.34) @ 22.08.26 08.18:52
2026-08-22 11:10:48.479 🔵 -[TSLocationRequestService requestLocation:]_block_invoke [CACHED] getCurrentPosition satisfied from lastLocation

11:23:41 — same call, same result (the 08:18:52 fix again, now 3 h 05 old):

2026-08-22 11:23:41.022 🟢 -[TSLocationRequestService requestLocation:] [getCurrentPosition] maximumAge: 0
2026-08-22 11:23:41.022 🔵 +[TSLocationHelper pickBestLocationBetween:and:desiredAccuracy:] desiredAccuracy: 5.000000
- A: 📍<+46.25xxxxxx,+6.07xxxxxx> +/- 2.00m ... @ 22.08.26 08.18:52
2026-08-22 11:23:41.022 🔵 -[TSLocationRequestService requestLocation:]_block_invoke [CACHED] getCurrentPosition satisfied from lastLocation

The same frozen location also drives geofence evaluation:

2026-08-22 11:10:48.795 🔵 -[TSGeofenceManager onEvaluate] - enabled? 1, _lastLocation? <+46.25xxxxxx,+6.07xxxxxx> +/- 2.00m ... @ 22.08.26 08.18:52

Analysis (as far as we can see from the log)

Two compounding behaviors:

  1. maximumAge: 0 seems to mean "disabled", not "must be fresh".
    trySatisfyWithLocation prints maximumAge: 0 ms and still satisfies the
    request from a fix recorded 2 h 52 earlier. If 0 is intentionally "no
    constraint", the docs don't make that clear and there is then no way to
    ask for a strictly-fresh sample other than passing maximumAge: 1.

  2. The request-service lastLocation never updates to fresher-but-less-
    accurate fixes.
    The stale 08:18:52 fix has ±2.00 m; the fresh fixes
    around 11:10 have ±3.82–4.46 m. pickBestLocationBetween (called with
    desiredAccuracy: 5.0) appears to compare candidates on accuracy only,
    with no freshness term so once a very accurate fix is cached, hours of
    subsequent (slightly less accurate) fixes never replace it, and with
    maximumAge disabled it is served as "current" indefinitely.

Impact on our app

The resurrected fix is also broadcast to onLocation subscribers, so it:

  • landed inside an actively-recorded trip (a 130 km teleport segment in the
    polyline),
  • was returned to a user-triggered "check-in" feature, which recorded the user
    at their morning departure point instead of their actual location,
  • kept re-centering the live map on the stale position after every pause.

Workaround we deployed

  • Pass an explicit maximumAge: 60000 on every getCurrentPosition call, and
  • verify location.age on every result / onLocation event in Dart, dropping
    anything older than 2 minutes.

Expected behavior

  • getCurrentPosition with maximumAge: 0 (or at least with a small explicit
    maximumAge) should never be satisfied from a cache older than that age,
    it should sample, and the "best location" choice should weigh freshness,
    not accuracy alone.

Full (unredacted) native log available on request the excerpts above redact
only the home coordinates.

Plugin Code and/or Config

bg.BackgroundGeolocation.ready(bg.Config(
  reset: true,
  geolocation: bg.GeoConfig(
    geofenceProximityRadius: 1000,
    geofenceInitialTriggerEntry: false,
    geofenceModeHighAccuracy: true, // Android-only; no-op on iOS
    desiredAccuracy: bg.DesiredAccuracy.high,
    filter: const bg.LocationFilter(
      policy: bg.LocationFilterPolicy.adjust,
      useKalman: true,
      trackingAccuracyThreshold: 50,
      maxImpliedSpeed: 60,
    ),
    stationaryRadius: 50,
    distanceFilter: 10,
    showsBackgroundLocationIndicator: true,
    activityType: bg.ActivityType.other,
    locationUpdateInterval: 30000,
    fastestLocationUpdateInterval: 10000,
    elasticityMultiplier: 1.0,
    stopTimeout: 3,
    locationAuthorizationAlert: {/* localized strings */},
    disableLocationAuthorizationAlert: true,
  ),
  app: bg.AppConfig(
    stopOnTerminate: false,
    startOnBoot: true,
    enableHeadless: true,
    notification: bg.Notification(
      title: 'Xxx', text: 'Tracking active',
      channelName: 'Location', sticky: false,
      priority: bg.NotificationPriority.low,
      smallIcon: 'drawable/ic_stat_xxx', color: '#4D2ADD',
    ),
  ),
  backgroundPermissionRationale: bg.PermissionRationale(/* strings */),
  locationAuthorizationRequest: 'WhenInUse',
  logger: bg.LoggerConfig(
    logLevel: bg.LogLevel.verbose, // verbose was ON during the incident
    logMaxDays: 3,
  ),
));
// Right after ready(): if the OS already granted Always, we realign via
// setConfig({locationAuthorizationRequest: 'Always'}). 14 geofences
// (hysteresis pairs) are registered at startup. iOS "Always" was granted.

Relevant log output

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions