Pitfalls to avoid when implementing push notifications

What to do and what not to do when implementing push notifications in your app, and elaborating on the concept of pull notifications to supplement push notifications.

Toni Sučić
11 min readNov 18, 2019

A note to the reader: I use the term pull notification in this article to refer to notifications that are stored in the database on the backend. When I originally wrote this article I used the term “message notification” to refer to such notifications, however, I later changed it to “pull notification” since this name makes more sense intuitively and forms a dichotomous relationship with the term “push notification”.

When implementing push notifications for your mobile app there are certain pitfalls you want to avoid.

  • Don’t start with designing the push notification. Start with the event that will trigger it, then consider whether you need a push notification, a pull notification, or both.
  • Don’t directly store the payload of the push notification in the database. The title and/or the body of a push notification should never be stored in the database, since it might change in the future or be translated into other languages.
  • Don’t store a single device token per user. Design your database such that a user can have multiple device tokens. Some users have multiple devices (like developers who often have a test device in addition to their personal device). Create a strategy for purging invalid tokens. People get new phones and lose or break old ones, rendering old tokens orphaned/invalid. You can read more about how to do this with Firebase Cloud Messaging here.
  • Remember to remove the device token with a call to the backend when the user logs out lest they continue to receive push notifications in spite of not being authenticated.
  • If you’re sending out reminder notifications at a given time of day, don’t use a time-based job scheduler to schedule a function that runs once every 24 hours. It won’t work across different time zones.
  • Don’t confuse push notifications with pull notifications and vice versa when conversing with your team.
Push notifications are typically associated with your lock screen

The duality of notifications

The way I see it, there are two types of notifications: push notifications and pull notifications. A pull notification is deceptively similar to a push notification in that like a push notification, it also has a title and a body, but unlike a push notification, a pull notification is persisted to the database and can be fetched by the app later. Pull notifications don’t appear on your lock screen, but they are viewed in your app instead.

  • Push notifications are ephemeral and static. They’re dispatched according to the «fire and forget» principle and are not stored in the database. It’s possible to update (“collapse”) push notifications on the lock screen by using a collapse ID/key.
  • Pull notifications are permanent and dynamic. They’re templates that are populated with data from the database and then either provided to the client through a REST API, or used in the payload of a push notification.

Push notifications are derived from pull notifications — not the other way around (i.e. you don’t store the payload of the push notification in the database. You only store the notification type, which is tied to a template in your business logic. This template can then be used to render a pull notification, which can be used to create a push notification). In some cases you might want to send a push notification that’s not tied to a pull notification on the backend, in which case you’d simply create a payload for it without creating a pull notification.

Pull (Message) notification & Push notification matrix

A pull notification might look something like this:

Title: Group update
Body: Steve shared a link in the group “Bicycle group”.

Looks a lot like a push notification. The pull notification also has a title and a body, but the key difference is that a pull notification doesn’t appear on a user’s lock screen like a push notification. It typically appears in the form of a notification list/feed in the app. Pull notifications are templates, as can be seen in the body of our example: "{user.firstName} shared an article in the group {group.name}." The phrasing of it might change in the future, new interpolated variables might be added, and at some point it might be necessary to translate it into other languages:

  • Steve delte en lenke i gruppa “Bicycle group”.
  • Steve hat einen Link in der Gruppe “Bicycle group” geteilt.

You’ll therefore not find this text in the database. It’s dynamically generated from the first_name field in the user table and the name field in the group table. The definition of the template and the variables on which it depends are defined in a function in the business logic layer of the application (possibly in the model). The database has no idea what the text looks like. It just knows what type of notification it is and which tables in the database it depends on.

In a relational SQL database this setup might look something like this (with the pull notification being named message notification in this case):

Notice how the rows in the message notification table don’t store any information at all about the title and the body. The message_notifications table only has a type that shows what kind of notification it is, when it was created and foreign keys to other tables it depends on (simplified a bit for the sake of example). In a more complex setup you could include a table that stores whether a user has enabled or disabled a given type of push notification.

Table for storing which notification types will trigger a push notification for a given user

In order to generate the title and the body for the pull notification, you need to program a function on the backend that looks at the type of the notification and generates the appropriate strings for it. If the pull notification depends on data stored in the database like in the example above, you need to query the database for that data and interpolate it in your string. This could be anything from someone’s name to the number of likes a post has received.

Dependencies like this that are fetched from the database introduce another layer of complexity, because you have to decide on what happens when the data you are querying gets deleted. Should the pull notification be deleted as well, or should it display something like "<deleted user> shared a link in the group <deleted group>." or some variation thereof? There might also be pull notifications that depend on more than a user and a group, and in that case you’d need to add another foreign key column to the message_notifications table for that dependency. The foreign keys should be nullable in case a given notification type requires fewer dependencies (like the “achievement” notification above), or in case the referenced foreign key doesn’t exist anymore.

Once you have defined the pull notification it’s time to consider whether it makes sense to also send an accompanying push notification. If it does, you simply take the text you just dynamically generated and put it in the notification payload. You should also include the pull notification ID in the payload so that the client knows which pull notification the push notification it just received is tied to on the backend.

If the notification is localized you’ll additionally need to store the locale of the user in the user table so that the backend knows into which language the template should be translated before it’s written to an HTTP response or put in the payload of a push notification.

Notification feed

Your app might need a feed or a list where the user can view a history of their notifications. Facebook has the notification bell 🔔 which will display a list of notifications once you tap on it. When you tap on a given notification in the list it will take you to another screen that’s relevant to the notification. For instance, if someone accepted your friend request on Facebook and you got a notification for that, tapping on it would take you to their profile page.

When the backend provides you the notification list it can do so in two ways: it can either include all the data that’s needed to display the contextually relevant screen when you tap on it, or it can include IDs instead which would require the user to do another request to the backend to get the necessary data to display the right screen (e.g. a user’s profile page or a post in a group).

Collapsing push notifications

If multiple people liked your picture on Instagram, it wouldn’t make sense to store a new pull notification for each like, nor would it be a good idea to send a push notification for each of them, since any user with a large following would have their lock screen bombarded with push notifications.

For this reason, all social media apps should have one pull notification for all the likes of your post. Since pull notifications are dynamic, you can just query the number of likes for a given post and interpolate them in the title or body of the pull notification. So at first it might look like “Steve liked your post”, but after a few more likes it would look like “Steve, Tim, Craig and 4 others liked your post”. Since push notifications are derived from pull notifications, you can easily send a new push notification with the updated text for each new like, but alas, that would result in a spammed lock screen.

Fortunately there’s a way of coalescing multiple push notifications into a single push notification. This is achieved by “collapsing” the existing notification and letting a new one take its place. APNs has the concept of an apns-collapse-id, and GCM has a collapseKey. Both can be used to replace an existing push notification that’s either in flight or that has already landed on the user’s lock screen, or it could even be used to remove a push notification from the lock screen (e.g. if a user unlikes a post right after having liked it). The end result is that the user is given the impression that an existing push notification on their lock screen was refreshed with new information.

This is a lot better than spamming the lock screen with many different notifications, but it’s not perfect since new notifications will continue to appear for each new like after the user has cleared the previous push notification they got. To solve this you can implement some kind of logic that “throttles” the number of push notifications sent to the user the more likes they get. So every time the number of likes increases by an order of magnitude, the frequency by which push notifications are sent out will be reduced.

Viewed notifications

It’s common for apps to keep track of which notifications you’ve viewed. On Facebook, notifications you’ve previously tapped on will have a white background, whereas notifications you haven’t tapped on will have a light blue background.

A naïve approach would be to store this information on the client, but the problem with that is that the viewed-state wouldn’t carry over to other devices, nor would it be retained if you reinstalled the app or got a new device.

The viewed-state can easily be stored on the backend simply by adding a viewed column to the message_notifications table.

message_notifications table extended with “viewed” column

You could also query and count all message notifications in the past 24 hours where viewed = FALSE, and supply that count to the badge field in the push notification payload so that the user will see a badge on the app icon that reflects the number of unviewed notifications.

Scheduled notifications and time zones

Some apps might need push notifications to be sent out at a specific point in time, e.g. a meditation app reminding its user to meditate every evening at 8 p.m or a reminder app issuing a reminder at a user-specified date and time. In cases like this, it’s important to consider the impact time zone changes could have on the user experience, and whether it would make sense to design your backend to be time zone aware.

Because people travel to other time zones, and due to daylight saving time, your app may end up sending push notifications at the wrong time if the user’s time zone isn’t accounted for. Since the backend doesn’t know in which time zone the user is located, the app needs to report it to the backend. You could either let the user specify their time zone in the app manually, or you could make the app detect it automatically and send it to the backend whenever it changes.

It’s considered good practice to use UTC time on the backend exclusively and rely on time zone libraries for time zone conversions. The rules governing time zones in different countries are quite complex, as illustrated in this video.

If you need to send out reminder notifications every evening at 7 p.m. you can do so by checking the time zone stored on each user before sending them out. That way it doesn’t matter whether it’s standard time or daylight saving time, or whether the user is on vacation in New Zealand or at home in Norway — the user will always get their reminder notification at 7 p.m.

If you’re using a time-based job scheduler (like cron) to send out the reminder notifications, you can schedule a function that enumerates all the users on this schedule (cron syntax 0,30,45 */1 * * *. More on why I picked that particular schedule here), then you can send a push notification to the ones whose time zone indicates that it’s 7 p.m. from their point of reference.

Note: There are exceedingly few territories where the offset is 45 minutes, namely the city of Eucla in Australia, Chatham Islands (New Zealand), and the country of Nepal. If you’re convinced that your app won’t be used in these territories, the simpler schedule */30 * * * * would suffice.

Conclusion

Push notifications are notoriously difficult to implement since there’s more to them than meets the eye. In a more complex app it’s not immediately obvious how to approach the task of implementing them. In this article I’ve hopefully set forth a framework that you can use as a foundation for implementing them in your app.

An important thing to keep in mind when implementing push notifications is that users tend to be averse to them in general since they’re so oft-abused. Overuse of push notifications is one of the main drivers behind app-uninstalls, so don’t go overboard with them. Only implement push notifications that will add value for the user, and give the user the option to enable or disable them as they see fit. You should also consider whether using provisional authorization for trial notifications might be a good idea.

--

--