The iOS 20-Region Limit — and How to Monitor Unlimited Geofences
You add geofencing to an iOS app, it works great with a handful of zones… and then you add your 21st store, campus, or delivery area and it silently stops firing for the new ones. This is one of the most common — and most confusing — walls in mobile location work. Here's exactly what the limit is, why it exists, and the pattern that scales past it.
What the limit actually is
iOS region monitoring runs through CLLocationManager. You register a circular region with startMonitoring(for:), and the OS watches it for you — even when your app is suspended or not running — waking you on enter/exit. That "even when the app isn't running" magic is exactly why it's capped: the system, not your app, holds the regions.
Apple's documented limit is 20 regions per app. Register a 21st and you don't get an exception — you get a locationManager(_:monitoringDidFailFor:withError:) callback (often kCLErrorRegionMonitoringFailure), and that region simply isn't watched. It fails quietly, which is why so many teams ship it and only discover the gap in production.
Why it exists (and why "just raise it" isn't an option)
Region monitoring is a shared system resource. The OS keeps your regions armed 24/7 against low-power location signals so it can wake a suspended app. Twenty is the budget Apple gives each app so the device's battery and the shared monitoring subsystem stay sane across every installed app. There's no entitlement or API to increase it — the number is fixed.
Workarounds that don't scale
- Cram everything into significant-location-change only. You can wake on big movements and do your own distance math, but you lose the OS's precise, low-power enter/exit at the boundary — accuracy and battery both suffer.
- Manually toggle regions in app code. The right idea, wrong layer: doing it ad-hoc in the view controller gets fragile fast (cold starts, permission changes, boundary flapping), and it doesn't survive relaunch cleanly.
The pattern that works: server-managed “nearest-N” sync
The scalable approach separates where your zones are (unlimited, on a server) from what the device monitors right now (the nearest handful, within the OS cap):
- Keep the full zone list server-side — thousands of them, no device limit.
- On launch and on movement, ask the server for the nearest N zones to the user (register ~18, not the full 20 — leave headroom for transient regions).
- Register exactly those with
startMonitoring(for:); remove the ones that dropped out of range. - Re-sync as the user travels. Use significant-location-change (or a "re-sync after X meters" rule) to re-fetch the nearest set when they move far enough that a different zone is now closest.
To the user it looks like every zone is always monitored; in reality the device is always watching the 18 that could plausibly matter next.
A minimal Swift sketch
func syncNearest(to location: CLLocationCoordinate2D) async {
// 1) Ask your backend for the nearest zones (server holds them all).
let zones = try await api.nearestZones(lat: location.latitude,
lng: location.longitude,
limit: 18)
// 2) Clear what's currently monitored, register the fresh set.
for region in manager.monitoredRegions { manager.stopMonitoring(for: region) }
for z in zones {
let region = CLCircularRegion(
center: CLLocationCoordinate2D(latitude: z.lat, longitude: z.lng),
radius: z.radius, identifier: z.id)
region.notifyOnEntry = true
region.notifyOnExit = true
manager.startMonitoring(for: region)
}
}
// Re-run syncNearest() on significant location change so the
// "nearest 18" follows the user as they travel.
What about polygon zones?
Core Location only monitors circles — there's no polygon region API. The workaround: store the polygon on the server, register its bounding circle on the device so the OS still wakes you nearby, and run a point-in-polygon check against the user's actual coordinate on entry to confirm they're truly inside before you act. You get arbitrary shapes without giving up OS-level wake-ups.
BOOT/updates; debounce boundary flapping (GPS jitter at the edge firing enter/exit repeatedly); apply a dwell threshold so a quick pass-through doesn't count; and remember background enter/exit needs Always location permission.
The short version
The 20-region cap is permanent, so stop fighting it. Hold every zone on a server, register only the nearest ~18 on the device, and re-sync as the user moves. Add point-in-polygon for non-circular areas and debounce the edges. That's the whole trick to "unlimited" geofences on iOS — and the same idea (with its own quirks) is what you need on Android too.
Don't want to build all that?
GeofenceKit does exactly this — server-managed nearest-N sync, polygon support, offline queue, background delivery — as one SDK for iOS, Android, React Native, and Flutter. Register thousands of zones; we keep each device within the OS limit for you.
Start free — get an API keyRelated: Android geofences & reboot · Background geofencing in React Native