Skip to content

Geofencing

BGeo geofences are app-facing circular regions you define — Transistor-shaped CRUD, distinct from the engine’s own internal wake geofence used for killed-app relaunch. You register them with an identifier, a centre, and a radius; the SDK tells you (via onGeofence) when the device enters, exits, or dwells inside one.

Geofences can be added, removed, and listed at any time — tracking does not need to be running. OS-level monitoring, however, is only active while tracking is started (start()): stop() unregisters every app-facing region from the OS (the persisted set is untouched), and start() re-registers them. Forcing a motion-state change with changePace() has no effect on geofence monitoring either way — only tracking start/stop does.

Adding and removing geofences

Full method signatures, parameters, and return types are documented on the Methods reference; this section covers the fields and the validation rules behind INVALID_GEOFENCE.

A geofence rejects with INVALID_GEOFENCE unless all of the following hold:

  • identifier is a non-empty string.
  • radius is a finite number greater than 0.
  • latitude is finite and within -90..90.
  • longitude is finite and within -180..180.
  • at least one of notifyOnEntry, notifyOnExit, notifyOnDwell is true (a geofence that notifies on nothing is rejected rather than silently registered as dead weight).
await BackgroundGeolocation.addGeofence({
identifier: 'home',
radius: 150,
latitude: 52.2297,
longitude: 21.0122,
notifyOnEntry: true,
notifyOnExit: true,
notifyOnDwell: true,
loiteringDelay: 300000, // 5 minutes, required for a DWELL transition
extras: { kind: 'home', label: 'Home' },
});

extras is an arbitrary object you attach to the geofence; it is never inspected by the SDK and is echoed back verbatim on every GeofenceEvent so you can identify which geofence fired without a lookup.

Adding a geofence whose identifier already exists overwrites it in place — new radius, coordinates, notify flags, and extras replace the old ones, and if it’s currently OS-registered it’s re-registered with the fresh values. There is no separate “update” call.

addGeofences() is the bulk form — same validation, same overwrite semantics, applied to a whole array in one native round-trip:

await BackgroundGeolocation.addGeofences([
{ identifier: 'home', radius: 150, latitude: 52.2297, longitude: 21.0122, notifyOnEntry: true, notifyOnExit: true },
{ identifier: 'office', radius: 100, latitude: 52.2319, longitude: 21.0067, notifyOnEntry: true, notifyOnExit: true },
{ identifier: 'school', radius: 120, latitude: 52.2189, longitude: 21.0289, notifyOnEntry: true, notifyOnDwell: true, loiteringDelay: 600000 },
]);

Removing is either targeted or total — there is no “remove several by id” call:

await BackgroundGeolocation.removeGeofence('office'); // just this one
await BackgroundGeolocation.removeGeofences(); // ALL geofences, whole persisted set

getGeofences() and geofenceExists() read the full persisted set — not just the proximity-sliced subset currently registered with the OS (see Proximity slicing below):

const all = await BackgroundGeolocation.getGeofences();
const hasHome = await BackgroundGeolocation.geofenceExists('home');

Radius guidance

There’s no platform-enforced minimum radius in this SDK — any value greater than 0 passes validation. In practice, a larger radius makes ENTER/EXIT more reliable: GPS accuracy jitter near a small boundary can produce spurious crossings, especially indoors or downtown. A tighter radius gives more precise triggering at the cost of occasional missed or bouncing transitions. Pick the radius per use case rather than a single fixed number.

One concrete platform constraint worth knowing: iOS clamps every region’s radius to CLLocationManager.maximumRegionMonitoringDistance at registration time, silently shrinking an oversized radius to that cap. Android has no equivalent clamp.

Geofence events: ENTER, EXIT, DWELL

Transitions are delivered through onGeofence as a GeofenceEvent — see that reference for the full payload shape. This section covers only the semantics of the three actions.

  • ENTER fires when the device crosses into the region, if notifyOnEntry is true.
  • EXIT fires when the device crosses back out, if notifyOnExit is true.
  • DWELL fires after the device has remained inside the region for at least loiteringDelay milliseconds, if notifyOnDwell is true. A notifyOnDwell geofence without a positive loiteringDelay falls back to a 5-minute (300000) default rather than disabling DWELL.

DWELL is implemented differently per platform, though the observed event is identical:

  • Android uses the OS’s own native dwell detection (Geofence.GEOFENCE_TRANSITION_DWELL + setLoiteringDelay) — Play Services tracks the loiter internally and delivers the transition.
  • iOS has no native dwell transition, so the SDK emulates it: on ENTER, it records the entry time and arms a timer for loiteringDelay; EXIT cancels the pending DWELL. Because a timer dies if the app is suspended before it fires, the pending entry is also re-checked on every accepted location fix and on wake — so a DWELL can arrive a little late on iOS, but it always arrives once the device has genuinely stayed long enough and is still inside on the next check.

A DWELL-only geofence (notifyOnDwell: true, notifyOnEntry/notifyOnExit both omitted or false) still requires entry monitoring under the hood to establish the loiter baseline on both platforms, but you won’t receive an ENTER event for it — only the eventual DWELL.

Proximity slicing

Both iOS and Android impose a hard cap on how many geofences an app can have registered with the OS at once — 19 on iOS, 99 on Android (each platform’s real budget is 20 / 100, minus one region the SDK’s own internal wake geofence occupies). Persisted geofences aren’t limited to that count — you can store as many as you like — but only a subset can be live with the OS simultaneously. The SDK bridges this gap with proximity slicing:

  • Of all persisted geofences, only those within geofenceProximityRadius metres of the device’s last accepted fix are candidates for OS registration (default 1000 m).
  • Candidates are sorted by distance and the nearest ones are registered, up to the platform budget — or a smaller number if you set maxMonitoredGeofences to deliberately stay under budget.
  • On every accepted location fix, the slice is re-evaluated: geofences that fell out of range are unregistered and newly-in-range ones are registered, keeping the OS-registered set centred on where the device actually is.
  • Before any fix has been received (cold start), the SDK falls back to registering the first geofences in persisted order, re-slicing as soon as the first fix arrives.
flowchart LR
    store[(All persisted geofences)] --> filter[Proximity filter\nwithin geofenceProximityRadius\nof last fix]
    filter --> sort[Sort by distance]
    sort --> cap[Cap at min of\nmaxMonitoredGeofences and\nplatform budget: 19 iOS / 99 Android]
    cap --> registered[OS-registered subset]
    registered -.-> change[onGeofencesChange: on / off delta]
    fix[New accepted fix] -.-> filter

onGeofencesChange tells you exactly which geofences are in the active, OS-registered set at any moment: it’s a delta, not the full set — on lists geofences newly registered, off lists geofences just unregistered, fired whenever the slice rotates. See GeofencesChangeEvent for the payload shape.

Synthetic ENTER on registration

geofenceInitialTriggerEntry (default true) requests a synthetic ENTER for any geofence that turns out to already contain the device at the moment it’s registered with the OS (iOS requestStateForRegion, Android INITIAL_TRIGGER_ENTER). Without it, a geofence you’re already standing inside when it’s added — or when proximity slicing brings it into the OS-registered set — would otherwise never fire ENTER until you leave and re-enter. Set it false if you only want events for boundary crossings that happen after registration.

Persistence

Geofences persist in the SDK’s own SQLite database (bgeo.db, in the geofences table) on both platforms, alongside the location queue — see the data pipeline concept. They survive app restarts and device reboots: the full set is reloaded and re-registered with the OS the next time tracking starts, with no need to call addGeofence() again after every launch.

Geofence ENTER/EXIT/DWELL transitions ride the same upload queue as location records rather than a separate channel — a transition is queued and uploaded exactly like a location fix, including delivery while the app was killed at the moment it happened. Your server receives it as a location-shaped record with an event field identifying it as a geofence transition, not a plain location.

Try it in the example app

The example app lets you create a geofence by long-pressing anywhere on the map, which opens a form pre-filled with the tapped coordinates.

The form takes an identifier, a radius in metres, and a switch per transition (ENTER, EXIT, DWELL); saving it registers the geofence and draws it on the map as a circle, with the Geo layer toggle controlling whether registered geofences are drawn at all.

iOS: the example app's New geofence form pre-filled with the long-pressed coordinates (identifier, radius, notify on ENTER/EXIT/DWELL switches, Save), and the resulting geofence drawn on the map as a circle around the pin.

iOS — the creation form, and the saved geofence on the map.

Android: the same New geofence form and the saved geofence drawn as a circle on the map alongside the tracked route.

Android — the same flow.