How to fix iOS push subscriptions getting terminated after 3 notifications
Tags: webdev, pwa, javascript
Published: 2023-06-30 03:28:49

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](https://progressier.com) 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](https://developer.mozilla.org/en-US/docs/Web/API/PushEvent) 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()](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/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()](https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil). This is a common issue. Even the [sample code on MDN](https://developer.mozilla.org/en-US/docs/Web/API/PushEvent) contains that problem:

![Faulty push event handler on MDN](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sqfs356cnhd4hi66ncuf.png)

 ❌ 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()](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/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()](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/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()](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification). Just remember to wrap that last bit in [event.waitUntil()](https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil) and remember that [event.waitUntil()](https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/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()](https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/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](https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe), all browsers require you to configure it as [userVisibleOnly](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscriptionOptions/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()](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/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](https://web.dev/push-notifications-handling-messages/)_ notification). 

![Screenshot from web.dev about the default Chrome push notification for silent push](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5yeqxuyds6fnea6xuft.png)

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](https://www.w3.org/TR/push-api/) 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()](https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/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](https://progressier.com).