---
title: Why Are Your Geofences Not Triggering on iOS?
description: iOS reports a geofence crossing only after the device leaves the boundary by a minimum distance and stays there 20 seconds. Here are the causes and fixes.
date:
  created: 2026-09-23
  updated: 2026-09-23
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor Geofences: sdks/capacitor/geofences.md
faq: true
---

# Why Are Your Geofences Not Triggering on iOS?

A geofence that fires on Android and stays silent on an iPhone is rarely a bug in your code. iOS reports a crossing only after the device has crossed the boundary, moved a minimum distance past it, and stayed there for at least 20 seconds, and it reports nothing at all when the device was already inside the region at registration time. Almost every report of a geofence not triggering on iOS comes down to one of six causes: that threshold rule, the already-inside case, the permission level, the radius, a reboot or relaunch, or listening to a live event instead of reading a durable queue. This guide works through each of them, with the [Capacitor Geofences plugin](../../sdks/capacitor/geofences.md) as the example.

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Build and deploy your Capacitor app with Capawesome Cloud" src="https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
  </a>
</div>

**Key takeaways:**

- iOS reports a crossing only after the device leaves the boundary by a system-defined minimum distance and stays there for at least 20 seconds.
- Registering a region the device is already inside produces no event on iOS, while Android fires an enter transition immediately.
- Only the Always authorization lets iOS launch a terminated app for a region event, and the upgrade prompt can be requested exactly once.
- Apple's stated expectation is a crossing reported "within 3 to 5 minutes on average, if not sooner", and region monitoring requires network connectivity.
- The `geofenceTransition` event never replays transitions detected while the app was terminated; the opt-in on-device queue stores nothing until `setConfig(...)` enables it.

## How iOS reports crossings

iOS applies three conditions to a boundary crossing before it reports one, and all three have to be met. [Apple's region-monitoring guide (archived)](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/LocationAwarenessPG/RegionMonitoring/RegionMonitoring.html){:target="_blank"} states the rule:

> Specifically, the user's location must cross the region boundary, move away from the boundary by a minimum distance, and remain at that minimum distance for at least 20 seconds before the notifications are reported.

Crossing the line is only the first of the three. The minimum distance is not a value you configure either, because "the specific threshold distances are determined by the hardware and the location technologies that are currently available". The same guide describes a second margin on top of the boundary: the system does not report a crossing until "the boundary plus a system-defined cushion distance is exceeded", which keeps a user walking along the edge of a region from producing a burst of enter and exit events.

Apple's only stated delivery expectation lives in the [`startMonitoring(for:)` reference](https://developer.apple.com/documentation/corelocation/cllocationmanager/startmonitoring(for:)){:target="_blank"}: an app "can expect to receive the appropriate region entered or region exited notification within 3 to 5 minutes on average, if not sooner". That paragraph still talks about iOS 6 and the iPhone 4S, so read it as an order of magnitude rather than a guarantee. It is still the right number to hold your field reports against, because an event that arrives four minutes after the crossing is documented behavior, not a failure.

Accuracy also depends on the radio environment. Region monitoring "requires network connectivity" to report changes in a timely manner, and the archived guide adds that "if Wi-Fi is disabled, region monitoring is significantly less accurate". Apple names the Simulator in the same passage, right before the threshold rule: "When testing your region monitoring code in iOS Simulator or on a device, realize that region events may not happen immediately after a region boundary is crossed." Confirm on a device by moving across the boundary and giving the system several minutes.

## Already inside at registration

Registering a region the device is already sitting inside produces nothing on iOS. [Apple's region-monitoring guide](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/LocationAwarenessPG/RegionMonitoring/RegionMonitoring.html){:target="_blank"} is unambiguous about it: monitoring starts immediately, "however, don't expect to receive an event right away, because only boundary crossings generate an event. In particular, if the user's location is already inside the region at registration time, the location manager doesn't automatically generate an event."

Android does the opposite. [`addGeofences(...)`](../../sdks/capacitor/geofences.md#addgeofences) triggers an enter transition right away when the device is already inside a newly added geofence, a difference the [announcement post](./announcing-the-capacitor-geofences-plugin.md#add-a-geofence) covers alongside the rest of the option set. The practical consequence is an onboarding flow that confirms itself on a Pixel and hangs on an iPhone: the app draws a fence around the user's current position, waits for the enter event to mark the setup as complete, and waits forever.

Treat the first transition you receive as the first crossing, not as proof that registration worked. To check registration itself, call [`getGeofences()`](../../sdks/capacitor/geofences.md#getgeofences), which returns the regions the operating system is currently monitoring. Core Location has `requestState(for:)` for asking whether the device is inside a region, but the plugin's public API does not wrap it, so decide the already-inside case from the position data you already have.

## Permission level

The authorization level decides whether iOS wakes your app at all. Apple's [authorization guide](https://developer.apple.com/documentation/corelocation/requesting-authorization-to-use-location-services){:target="_blank"} compares the two access levels line by line: an app with When In Use authorization is not launched by the system ("No. The user must launch the app."), while Always authorization launches a terminated app automatically "for significant location change, visits, and region monitoring services". A geofence that works with the app on screen and goes quiet afterwards is usually a When In Use app.

Getting to Always is a ladder with one-way steps. You request When In Use first and Always afterwards, and "you can make the request only once". Two answers from the user end the climb early:

- **"Allow Once" on the first prompt.** [Apple documents](https://developer.apple.com/documentation/corelocation/cllocationmanager/requestalwaysauthorization()){:target="_blank"} that Core Location then "ignores further calls to requestAlwaysAuthorization() due to the temporary authorization", so the upgrade prompt never appears and no error tells you why.
- **"Keep Only While Using" on the follow-up prompt.** Choosing "Allow While Using App" grants provisional Always, and iOS shows a second prompt later, typically while the app is not running. Answering it that way downgrades the app to When In Use, which is how geofences stop firing days after a permission flow that looked successful.

Both usage description keys belong in `Info.plist` before the first request, because "authorization requests fail immediately if the required keys aren't present". With the keys missing, [`requestPermissions(...)`](../../sdks/capacitor/geofences.md#requestpermissions) and `addGeofences(...)` reject with an explicit error. To install the Capacitor Geofences plugin and add the keys, refer to the [Installation](../../sdks/capacitor/geofences.md#installation) section of the plugin documentation; the plugin is part of [Capawesome Insiders](../../insiders/index.md), the paid subscription. Rather than assuming yesterday's grant survived, read the current state on every launch and request the background permission in its own call:

```typescript
import { Geofences } from '@capawesome-team/capacitor-geofences';

const hasAlwaysPermission = async () => {
  let status = await Geofences.checkPermissions();
  if (status.location !== 'granted') {
    status = await Geofences.requestPermissions({ permissions: ['location'] });
  }
  if (status.location !== 'granted') {
    return false;
  }
  if (status.backgroundLocation !== 'granted') {
    status = await Geofences.requestPermissions({
      permissions: ['backgroundLocation'],
    });
  }
  return status.backgroundLocation === 'granted';
};
```

With only the foreground permission granted, `addGeofences(...)` rejects with `PERMISSION_DENIED`. Once [`checkPermissions()`](../../sdks/capacitor/geofences.md#checkpermissions) reports `denied`, no prompt will appear again and [`openSettings()`](../../sdks/capacitor/geofences.md#opensettings) is the only route back.

## Radius and network

The radius decides whether the system can resolve the crossing at all, and the 200 meter figure that circulates in forum threads is widely misquoted. It is the distance past the boundary that Apple's guide tells you to assume when testing ("you can assume that the minimum distance is approximately 200 meters"), and it is the radius of the region in [Apple's own sample](https://developer.apple.com/documentation/corelocation/monitoring-the-user-s-proximity-to-geographic-regions){:target="_blank"}. Neither is a recommended radius, but together they set the scale: a 50 meter fence around a shop entrance asks the system to resolve a crossing well below the distance Apple's own testing guidance assumes. Treat 200 meters as the practical floor on iOS.

The only radius band Apple states anywhere is old wording in the `startMonitoring(for:)` reference, where regions "with a radius between 1 and 400 meters work better on iPhone 4S or later devices". It still refers to iOS 6, so it is a hint about the order of magnitude and nothing more.

Oversized regions fail rather than shrink. Apple documents that monitoring a region with a distance larger than [`maximumRegionMonitoringDistance`](https://developer.apple.com/documentation/corelocation/cllocationmanager/maximumregionmonitoringdistance){:target="_blank"} "causes the location manager to send a regionMonitoringFailure error to the delegate". The plugin clamps the radius to that value before registering, so a region that is too large becomes a smaller one instead of failing. That clamping is plugin behavior, not something iOS does for you.

Region monitoring also needs the device in a usable state. [Apple's region-monitoring guide](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/LocationAwarenessPG/RegionMonitoring/RegionMonitoring.html){:target="_blank"} lists the reasons it can be unavailable: the user denied the authorization, location services are off in Settings, Background App Refresh is off for the device or for your app, or the device is in Airplane mode. The Background App Refresh case produces the classic single-device mystery, because "the system doesn't wake your app for region notifications when the Background App Refresh setting is disabled globally or specifically for your app".

## Reboots and relaunches

A reboot and a relaunch are different events, and Apple documents them in different places. After a reboot, delivery does not resume until somebody picks the phone up, because [Apple's condition monitoring article](https://developer.apple.com/documentation/corelocation/monitoring-the-user-s-proximity-to-geographic-regions){:target="_blank"} states that "monitoring can only occur after the user unlocks the device after a reboot". An overnight OS update on a phone nobody touched until morning explains a night with no events on its own.

The region data itself survives. The [`monitoredRegions` reference](https://developer.apple.com/documentation/corelocation/cllocationmanager/monitoredregions){:target="_blank"} states that "the location manager persists region data between launches of your app. If your app is terminated and then relaunched, the contents of this property are repopulated with region objects that contain the previously registered data." What does not survive is the object graph inside your process. After a boundary crossing relaunches a terminated app, [Apple's `notifyOnEntry` reference](https://developer.apple.com/documentation/corelocation/clregion/notifyonentry){:target="_blank"} is explicit that "your app must configure new location manager and delegate objects to receive the notification", and the same condition monitoring article says it again for the newer `CLMonitor` API: "When your app relaunches, it's your responsibility to recreate the monitor with the same identifier."

[Apple's region-monitoring guide](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/LocationAwarenessPG/RegionMonitoring/RegionMonitoring.html){:target="_blank"} puts a budget on that relaunch: an app woken for a region event is "given a short amount of time (around 10 seconds) to handle the event". Ten seconds does not reliably cover a cold web view start plus a round trip into JavaScript, which is why the plugin does this part natively: it shows the geofence's local notification, appends the transition to the queue, and uploads it without the web view starting. On Android, the plugin re-registers geofences after a device reboot or an app update. On iOS, the monitored regions are persisted by the operating system.

## Transitions while terminated

The `geofenceTransition` listener is the usual reason transitions look lost rather than late. It is a live feed, delivered only while your app runs with a listener registered, and transitions detected while the app was in the background or terminated are never replayed. An app built on [`addListener('geofenceTransition', ...)`](../../sdks/capacitor/geofences.md#addlistenergeofencetransition-) alone sees exactly the crossings that happened while it was on screen.

The durable record is the on-device queue, and it is opt-in. Nothing is stored until [`setConfig(...)`](../../sdks/capacitor/geofences.md#setconfig) is called with `maxSize` or a `url`, and transitions detected before that call are not stored, so a fresh install that reads the queue on first launch gets an empty array by design. Enable it once, then drain it whenever the app becomes active:

```typescript
import { Geofences } from '@capawesome-team/capacitor-geofences';

const enableQueue = async () => {
  await Geofences.setConfig({ maxSize: 1000 });
};

const drainQueue = async () => {
  let hasMore = true;
  while (hasMore) {
    const result = await Geofences.getQueuedTransitions({ limit: 1000 });
    if (!result.transitions.length) {
      break;
    }
    await persist(result.transitions);
    await Geofences.deleteQueuedTransitions({
      upToId: result.transitions[result.transitions.length - 1].id,
    });
    hasMore = result.hasMore;
  }
};
```

The queue holds 1,000 transitions by default and drops the oldest first once it is full, and the `droppedCount` of [`getQueueStatus()`](../../sdks/capacitor/geofences.md#getqueuestatus) tells you whether that happened. A transition delivered live is also written to the queue, so treat the queue as the single source of truth and deduplicate by the monotonic `id` from [`getQueuedTransitions(...)`](../../sdks/capacitor/geofences.md#getqueuedtransitions).

If you configured an upload `url`, the upload drains that same queue, and the server's status code decides the fate of each batch. Anything outside `2xx` and the retryable set (`401`, `408`, `429`, `5xx`, timeouts and network errors) drops those transitions permanently, so a `400` from a half-deployed endpoint is indistinguishable from "the geofence never fired" when you only look at server data. The [announcement post](./announcing-the-capacitor-geofences-plugin.md#catch-up-on-missed-transitions-with-the-queue) covers the queue mechanics in full.

## Limits and unsupported events

Four remaining causes are platform limits rather than misconfiguration. iOS monitors at most 20 regions per app, and `addGeofences(...)` rejects with `GEOFENCE_LIMIT_EXCEEDED` beyond that, so region 21 never fires. The [announcement post](./announcing-the-capacitor-geofences-plugin.md#how-many-geofences-can-you-register) describes the strategy of registering only the regions nearest to the user.

Dwell transitions are an Android feature. On iOS, `androidNotifyOnDwell` and `androidLoiteringDelay` are ignored and only enter and exit transitions are reported, so a handler written for `TransitionType.Dwell` never runs on an iPhone.

The `latitude` and `longitude` of a transition are always `null` on iOS, because Core Location does not provide the location that triggered the crossing. Code that reads them as numbers can fail in a way that looks like a missing event. Look the coordinates up by the `geofenceId` instead.

There is no web implementation. Every method rejects with an unimplemented error in the browser, so a geofence tested in a dev server never fires at all.

## Troubleshooting checklist

Each row maps a symptom to its likely cause and the fix explained above.

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Nothing happens after adding the geofence | The device was already inside the region at registration | Leave the region and come back; only boundary crossings generate an event |
| You crossed the boundary and got no event | The threshold conditions were not met | Move a few hundred meters past the boundary and stay there for at least 20 seconds |
| The event arrives minutes after the crossing | Documented delivery latency | Expect 3 to 5 minutes on average; measure against that, not against seconds |
| Works in the foreground, silent after the app is swiped away | Only When In Use authorization was granted | Request `backgroundLocation` in a second call; `addGeofences(...)` rejects with `PERMISSION_DENIED` without it |
| The Always prompt never appears | "Allow Once" was tapped, or the `Info.plist` usage strings are missing | Add both usage description keys; after "Allow Once", further upgrade requests are ignored |
| It worked for days, then stopped without a code change | The second system prompt was answered "Keep Only While Using" | Read `checkPermissions()` on every launch and route the user to `openSettings()` |
| A small test fence never fires | The radius is below the distance the system can resolve | Use 200 meters or more on iOS |
| A very large region never fires | The radius exceeds `maximumRegionMonitoringDistance` | The plugin clamps it; split the area into several regions instead |
| Fires in the city, not in the countryside or in Airplane mode | Region monitoring requires network connectivity | Retest with a connection and expect significantly lower accuracy while Wi-Fi is off |
| Nothing fires on one device, everything on another | Location services, Background App Refresh or Airplane mode | Check both settings for the device and for the app |
| No events overnight after an OS update | Monitoring resumes only after the first unlock following a reboot | Nothing to fix in code; account for it when reading field reports |
| Transitions detected while the app was terminated never arrive | The queue was never enabled, overflowed, or an upload dropped the batch | Call `setConfig(...)`, drain with `getQueuedTransitions(...)`, and watch `droppedCount` |

## FAQ

### Why aren't my geofences triggering on iOS?

Most of the time one of the threshold conditions was not met. iOS reports a crossing only after the device crosses the boundary, moves a minimum distance past it, and remains there for at least 20 seconds, and it reports nothing when the device was already inside the region at registration. The next most common causes are When In Use instead of Always authorization, a radius below 200 meters, and an app that listens to the live transition event without enabling the durable queue.

### How long does iOS take to report a crossing?

Apple's documented expectation is "within 3 to 5 minutes on average, if not sooner", and that number comes with a network requirement, since the region monitoring service needs connectivity to report changes in a timely manner. A test that gives up 30 seconds after crossing the line proves nothing. Walk or drive past the boundary, wait five minutes, and check the queue afterwards.

### Do geofences still fire after a force quit?

Yes, with Always authorization. iOS relaunches a terminated app in the background to handle the boundary crossing and gives it around 10 seconds to do so. The plugin handles that wake-up natively, showing the local notification and appending the transition to the queue without the web view starting. The `geofenceTransition` event does not replay it, so read the queue when the app becomes active again.

### Do geofences survive a device reboot?

The region data does. The location manager persists region data between launches, and a relaunched app finds its previously registered regions. Delivery is the part that pauses, because monitoring can only occur after the user unlocks the device once after a reboot. On Android the plugin re-registers geofences after a reboot or an app update, while on iOS the monitored regions are persisted by the operating system.

### Why does it work on Android but not on iOS?

The most common reason is the already-inside case: Android reports an enter transition immediately when you add a geofence the device is already inside, so the same code confirms itself on Android and stays quiet on iOS. Add the iOS threshold rule and the Always authorization requirement, and one integration produces two very different sets of field results.

### Can I test geofences in the iOS Simulator?

Only partly. Apple names the Simulator in the same sentence that warns region events may not happen immediately after a boundary is crossed. Use it to verify that permissions are granted and that [`getGeofences()`](../../sdks/capacitor/geofences.md#getgeofences) returns your regions, then confirm delivery on a device by crossing the boundary and waiting several minutes.

### What radius should I use on iOS?

At least 200 meters. That number is not an Apple recommendation for the radius: it is the distance past the boundary Apple's guide tells you to assume for testing, and the radius of Apple's own sample region. Smaller regions are detected unreliably in the field. At the other end, the plugin clamps anything above `maximumRegionMonitoringDistance`.

## Stay in the loop

Plugin releases and the platform behavior changes that arrive with each new iOS version go out in the Capawesome newsletter first.

[Subscribe to the Capawesome Newsletter](https://capawesome.io/newsletter/){ .md-button .md-button--primary }

## Conclusion

Before debugging your own code, make the setup match what iOS requires: Always authorization, a radius of 200 meters or more, the queue enabled through `setConfig(...)`, and a test that involves real movement and a few minutes of patience instead of a teleported Simulator pin. If all four hold and a crossing still goes missing, `getQueueStatus()` and the `uploadFailed` event tell you whether the transition was detected and then lost, or never detected at all.

[Announcing the Capacitor Geofences Plugin](./announcing-the-capacitor-geofences-plugin.md) walks through the full API, and [Announcing the Capacitor Background Geolocation Plugin](./announcing-the-capacitor-background-geolocation-plugin.md) covers the case where you need the trail between two crossings rather than the crossings themselves.

If you have any questions, join us on the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}. To stay updated on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
