With iOS 16.4, Apple has added push notifications to installed PWAs. However, you may have noticed that iOS push subscriptions appear to be automatically terminated after 3 push notifications.
You send a notification, then a second, then a third. Everything works fine. However, when sending the fourth, everything breaks.
Well, the good news is that it's fairly easy to fix!
TL;DR: It's a problem with your push implementation.
Of course, push subscriptions are never automatically terminated when implemented correctly. This would defeat the whole purpose of the feature!
(Side note: if you don't want to bother with these boring technical implementation details, go ahead and sign up for Progressier instead).
How to fix push subscriptions being terminated on iOS Safari
When you send a push notification to a subscribed user, the service worker for the matching domain fires a push event. The push event handler in your service worker is responsible for showing the notification to the user.
However, one very important detail that many implementations are missing is that your event handler must absolutely show the notification before the event itself terminates.
Notifications are displayed using registration.showNotification(). This function returns a Promise that resolves once the notification has been shown to the user.
Almost all cases of push subscriptions being terminated by Safari can be explained by push event handlers missing event.waitUntil(). This is a common issue. Even the sample code on MDN contains that problem:
❌ Incorrect:
self.addEventListener("push", function(e){
self.registration.showNotification(e.data.title, e.data);
});
If you use the sample code above, push notifications won't work properly in the long run on iOS because the promise returned by registration.showNotification() will often resolve after the push event handler has finished.
❌ Also incorrect:
self.addEventListener("push", async function(e){
await self.registration.showNotification(e.data.title, e.data);
});
Making the event listener asynchronous and awaiting the registration.showNotification() promise to resolve looks like a very tempting solution. But you'll still run into the same issues.
Here is why: it's not enough for your event handler to wait for the work to be finished. You have to proactively tell the service worker what the event itself requires to be considered finished. This is counterintuitive because you rarely (if ever?) see that sort of behavior anywhere in client-side or server-side logic.
✅ Correct:
self.addEventListener("push", function(e){
e.waitUntil(
self.registration.showNotification(e.data.title, e.data)
);
});
This is a simplified example. In most cases, your code will do a few other things before invoking registration.showNotification(). Just remember to wrap that last bit in event.waitUntil() and remember that event.waitUntil() expects a Promise (not a function) as an argument.
So why are push notifications working in other browsers if my implementation is faulty?
This is because of a concept called silent push. A silent push occurs when the browser receives a notification but fails to display it to the user (or does so with a delay).
When you omit event.waitUntil(), Safari considers the notification to be a silent push because the event terminates before the notification is displayed (even if it is shown shortly thereafter).
Silent push has always been frowned upon across the board. In fact, when you register a push subscription, all browsers require you to configure it as userVisibleOnly, meaning you guarantee that you will only send notifications that you will actually show to users.
This is a security measure to prevent apps to do work in the background without users knowing about it. If you accidentally do 3 of those on Safari, the push subscription is canceled. Any further attempts would result in errors.
Other browsers have a different way of handling this. For example, Chrome does not invalidate push subscriptions, so as long as you call registration.showNotification() before the event handler terminates, you'll likely bypass Chrome's anti-silent-push security measure (which is to show a default This site has been updated in the background notification).
What seemed to work in many apps for years was always wrong. Many developers are claiming Apple released a bad push implementation — but this is not what's happening here. Apple's implementation is arguably closer to the W3C specs than Google's. If your setup worked in other browsers, it's essentially because... you were able to avoid the issue by chance. In any case, implement event.waitUntil() properly now. You'll eliminate timing randomness and make your implementation more future-proof.
That's it for me! Hopefully, now you know how to get around push subscriptions being canceled after 3 push notifications. If you have any questions, feel free to contact me here.
Top comments (1)
This is correct. However the prohibition to use silent push is something that exists since the beginning. I even wrote an article about that many years ago:
blog.pushpad.xyz/2020/11/silent-pu...
Some comments have been hidden by the post's author - find out more