Local notifications get sold as one of iOS’s simplest APIs. You say “remind the user at 14:00”, the system reminds them. On a real device it’s messier. I’ve built on local notifications across three apps; here’s why delivery isn’t 100%, and what to do about it.
iOS notification throttling
Limits Apple doesn’t document, but I’ve observed:
– You can have at most 64 pending notifications in the queue per app. Schedule a 65th and iOS silently drops an older one.
– Three or more notifications firing in the same second, and some get dropped.
– With Low Power Mode on, delivery is also delayed, especially for silent notifications.
One of my users had set up 200 different reminders. Only the next 64 ever fired.
Fix: sliding window schedule
Instead of scheduling all notifications upfront, I only schedule the next seven days. A daily BGAppRefreshTask refreshes that window:
let center = UNUserNotificationCenter.current()
// Clear
let existing = await center.pendingNotificationRequests()
center.removePendingNotificationRequests(
withIdentifiers: existing.map(.identifier)
)
// Reschedule
let upcoming = reminders.filter {
let delta = $0.fireDate.timeIntervalSinceNow
return delta > 0 && delta < 7 * 24 * 3600
}
for r in upcoming {
let content = UNMutableNotificationContent()
content.title = r.title
content.sound = .default
let trigger = UNCalendarNotificationTrigger(
dateMatching: Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: r.fireDate),
repeats: false
)
let req = UNNotificationRequest(identifier: r.id.uuidString, content: content, trigger: trigger)
try await center.add(req)
}BGAppRefreshTask setup
// Info.plist: BGTaskSchedulerPermittedIdentifiers
// com.mycompany.app.refreshNotifications
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.mycompany.app.refreshNotifications",
using: nil
) { task in
Task {
await refreshNotifications()
task.setTaskCompleted(success: true)
}
scheduleNextRefresh()
}
func scheduleNextRefresh() {
let req = BGAppRefreshTaskRequest(identifier: "com.mycompany.app.refreshNotifications")
req.earliestBeginDate = Date(timeIntervalSinceNow: 24 * 3600)
try? BGTaskScheduler.shared.submit(req)
}Caveat: BGAppRefreshTask doesn't run on any guarantee. iOS decides based on user behaviour. For users who don't open the app, the refresh may never fire. In that case the app reschedules fresh on the next launch; the seven-day window still buys you a backup.
The repeating-notification trap
If you want a 9 a.m. daily reminder, UNCalendarNotificationTrigger(repeats: true) looks appealing. But then cancelling is painful: if the user wants to skip one day's reminder and keep the rest, you have to cancel the repeating trigger and add individual ones. Scheduling each one separately turned out to be more flexible for me.
Silent notification + state update
When a user switches to a different iPhone (iCloud restore), local notifications don't carry over. Fix: send a silent push with a state update, and reschedule the local notifications on-device. The backend only sends a "refresh your schedule" signal, never the content itself.
Time zone pitfall
Say the user sets a reminder at 9:00 while in Istanbul, then flies to London. What should the reminder do? In UNCalendarNotificationTrigger, if you don't set dateComponents.timeZone, the reminder fires at 9:00 in the device's current time zone (London). If you set it, it stays anchored to the original zone. Which is right? Ask the user. In my app the answer depends on the reminder type: medication reminders stay fixed, meeting reminders follow the time zone.
Measuring delivery rate
You can't directly measure whether a notification was delivered; you can only measure taps. Taps tell you nothing if the user saw it and swiped it away. The indirect path: on the next app launch, ask "your last notification fired at 14:00, did you see it?". That's how I discovered an early version of mine had about 87% delivery.
Final advice
If local notifications are core to the user's workflow, test on at least ten different device models for a week. Lower-RAM devices like the iPhone SE and iPhone 12 mini hit throttling more often. Ask TestFlight beta testers "which reminder did you miss?". Once your delivery rate hits 95% you're ready to ship.