Home / Blog / Local notifications: the 100% delivery myth

Local notifications: the 100% delivery myth

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 […]

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.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch