Handling Background and Foreground States for Reliable Notifications
Push notifications don’t always behave the same. Delivery depends on app state, device settings, and power rules. This post explains FCM and APNs differences, how foreground and background states work, and what to watch for to keep messages reliable.

Almost every app uses notifications, and it can be quite hard to troubleshoot when users complain they don’t see anything while everything works perfectly on developers' devices.
The problem is that notification delivery depends on many factors: application state (foreground, background, stopped), power-saving modes, device settings, and system-level throttling.
Let's review some concepts and nuances that should be taken into account when the goal is to have reliable notifications.
Firebase Cloud Messaging (FCM) and Apple Push Notification service (APNs)
For Android devices, push notifications are sent through Firebase Cloud Messaging (FCM), Google's platform that handles message delivery, retry logic, queueing, device registration management, etc.
Push notifications to iOS devices must go through Apple Push Notification service (APNs), but you can choose whether to integrate with APNs directly or use an intermediary service like FCM that manages the APNs connection for you.
APNs requires maintaining persistent HTTP/2 connections to Apple's servers, implementing token-based or certificate-based authentication, and handling platform-specific error codes and feedback. You can also use FCM to send messages to Apple devices, which will take care of some differences between the platforms. FCM abstracts this by acting as a proxy to APNs while exposing the same API used for Android, allowing you to send to both platforms with a single POST request format and authentication mechanism. However, you still have to pay attention to the Apple-specific payload structure.

The tradeoff is a slight latency increase and dependency on Google's infrastructure, but for most use cases, the unified interface and reduced implementation complexity outweigh these costs. In this article, we'll focus on using FCM for both platforms.
Notification messages
Have you faced such a situation: push notifications are integrated, but they work only when the app is minimized? This might not be obvious, but notifications are treated differently when the app is in the foreground and background.
The reason for that is that in the background case system takes care of notifications. But when the app is in the foreground, it’s up to the developer to decide how to handle it.
If you examine the FCM payload structure, you'll notice two key objects: data
and notification
.
When your app is in the background state and you send a message with the notification
object, the system automatically displays it to the user. However, when the app is in the foreground, the system won't automatically show the notification, instead, your app receives a callback and you need to handle the display yourself.
Data messages
These messages give developers full control over how notifications are handled. Unlike notification messages that are automatically displayed by the system, data messages let your app process the payload and decide what to do with it.
To send such messages in FCM, pass just the data
object without the notification
.
Message queueing
So, what happens if the device is offline?
FCM stores the messages in a queue for about a month. The queued message will be sent when the devices come back online.
Note that behavior for multiple queued messages varies: Data messages are non-collapsible by default unless you explicitly use collapse_key
(Android) or apns-collapse-id
(iOS). Notification messages have inconsistent collapsing behavior depending on how they're sent. Without explicit collapse keys, you may receive multiple separate messages when the device comes back online. When an iOS device is offline, APNs keeps only the most recent notification per app/device. Newer ones replace older ones, regardless of collapse IDs.
App termination and notification delivery
Android allows users to force stop apps, after which no background services are permitted to run for that app.
However, aren't notifications still handled at the OS level?
Yes, but this is done intentionally. If the user decides to “kill” the app, they don’t want it to do anything. So, the Broadcast receivers will unregister themselves until the user launches the app again. There’s no need in trying to restart the app if it was killed by the user.
However, if it was killed by the system, the broadcast receivers will still be registered, and the notifications will be handled.
On iOS, there's no force stop option. When users swipe up from the app switcher, they're only removing the app from the recent apps list, the app remains registered for push notifications.
Background work limitation
On Android, there is a Doze mode that restricts the background work of the apps.
Regular Priority FCM Messages:
- Delayed until maintenance windows
- Messages are batched and delivered together
It is also possible to send high-priority messages that bypass the limitations and give approximately a 10-second window to handle them. However, misuse of high-priority messages might lead to deprioritization.

iOS handles that differently. Silent pushes (with content-available: 1
) wake the app for about 30 seconds, but the system throttles them based on usage patterns, battery level, and app behavior. There's no guaranteed delivery time; they can be delayed for hours or dropped entirely.
Low Power Mode is particularly aggressive, blocking all background app refresh, including silent pushes. However, alert notifications (those with alert
, sound
, or badge
) are prioritized over silent notifications. Unlike Android's explicit priority levels, iOS uses undocumented logic to decide if or when to deliver silent push notifications. The rate limitation is undocumented to allow Apple the freedom to change the behavior. Sending too many background notifications (more than a few per hour) can cause the system to throttle delivery.
Token management
FCM registration tokens identify specific app installations for routing notifications. But these tokens should not last forever.
Imagine having thousands of users who haven't been online for several months. Sending messages to all of them would waste resources and increase costs. This is why tokens expire, and when a user comes back online, a new registration token is generated. If this is not handled properly, you risk losing entire user segments.
Implementation checklist
Now, let’s make a checklist of cases that should be taken into account when implementing push notifications to ensure that important messages won’t be lost:
- foreground and background notifications are shown
- high-priority notifications are not delayed
- token refresh is handled
- error cases and fallbacks are implemented
Debugging push notifications
When debugging, start with the basics: check if the token is fresh, verify the payload structure matches the app state, and look at actual device logs.
Device logs reveal what server logs cannot. Your backend might show a 200 response from FCM, but that only confirms FCM accepted the message, not that it reached the device. For example, that might happen if some users have stale FCM tokens (older than 270 days).
Monitoring push notifications
Besides device logs, you might also want to monitor the analytics on the pushes:
- delivery tracking
- open rate
FCM does provide some of that, but the functionality is limited. FCM tracks "Sends" and "Received", but these metrics can be delayed for up to 24 hours.
So, if you want better observability, you would have to either implement it yourself or use a 3rd party service that would manage that for you. For example, tools like Clix focus on just the essentials: token management, delivery analytics, and unified API across platforms, without the overhead of full-featured marketing platforms.
Conclusion
On paper, push notifications are trivial, your backend sends a message, then the user's phone displays it. In practice, you're dealing with platform differences, app states, power optimizations, token lifecycles, and manufacturer quirks all at once.
Most notification failures come from the same few places. Force-stopped apps on Android. Expired tokens that nobody noticed. Foreground handlers that were never implemented. Xiaomi (or other) phones treat swipe-to-close as a force stop.
Real devices are essential for testing. Emulators won't show you battery optimization issues or manufacturer-specific behaviors. Don’t forget to test it on devices from different manufacturers.