Understanding Device Reachability and Deliverability
To document how Firebase Cloud Messaging (FCM) reachability and deliverability work, drawing from real-world analysis and official Firebase documentation.
Key Points before diving in
Users: At WebEngage, anybody who has interacted with your business at least once is called a User.
Devices: Anybody interacted with your business at least once via Website or Mobile device has a unique device attached to them. A user can have multiple devices and each device has its own push reachability.
How WebEngage sends FCM to user's device?
The journey of a notification is an asynchronous chain. A "Success" response from the WebEngage API only confirms the start of this chain.
WebEngage Server: Validates the user segment and sends the payload to the Cloud Service Provider (CSP).
FCM: Accepts the message and checks if a persistent socket connection exists for that specific Device Token.
Transport Layer: If the device is "Offline" (no internet), the FCM stores the message in a TTL (Time-to-Live)buffer.
Android OS: Receives the packet via a background system process (Google Play Services).
SDK Layer: The WebEngage SDK intercepts the intent, downloads rich media (if any), and renders the notification.
Reachability
No FCM token available
About ~5% of devices show unreachable behaviour due to device-specific failures such as:
| Errors observed in empirical customer data while fetching FCM token |
|---|
| MISSING_INSTANCEID_SERVICE |
| TIMEOUT |
| AUTHENTICATION_FAILED |
| PHONE_REGISTRATION_ERROR |
| TOO_MANY_REGISTRATION |
| FIS_AUTH_ERROR |
| Firebase Installations getId Task has timed out. |
How to measure the impact?
Track an event FCMTokenError on WebEngage and analyze it in Event analytics to understand and assess the impact and frequency of similar events.
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
try {
if (task.isSuccessful) {
val token = task.result
WebEngage.get().setRegistrationID(token)
} else {
val message = hashMapOf(
"error" to (task.exception?.message ?: "Unknown error")
)
WebEngage.get().analytics()
.track("FCMTokenError", message)
}
} catch (e: Exception) {
e.printStackTrace()
val message = hashMapOf(
"error" to (e.message ?: "Unknown error")
)
WebEngage.get().analytics()
.track("FCMTokenError", message)
}
}Missing Android 13 Notification Permission
Android 13 (API Level 33) and above introduced a runtime POST_NOTIFICATIONS permission required before notifications can be shown (and therefore before FCM notifications are fully usable). Your app must request and handle this permission at runtime. (Google official docs)
How to make your device reachable on WebEngage?
Follow the doc to make your application compatible with Android 13 and above and for tracking reachability on WebEngage.
Deliverability -What Affects Message Delivery
Deliverability depends on numerous factors:
Integration Issues
Permissions
Missing or incorrectly handled Android 13 notification permission can block all notifications (even if token is valid). Follow the doc to make your application compatible with Android 13 and above and for tracking reachability on WebEngage.
Token Handling
Not handling onNewToken() properly can cause send attempts to stale tokens.
Follow the doc to pass refreshed token to WebEngage
Official docs stress storing and managing tokens effectively: Best practices include storing tokens on your server and updating whenever token changes. (Firebase)
Handling multiple Firebase service
If multiple Firebase services are implemented; at any given time, only one Firebase service will receive the message callback onMessageReceived.
How to debug?
- In the merged manifest check for services having intent-filters
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
- If you discover several services, find a means to construct a common service that passes the message payload to WebEngage as mentioned here.
Device side failures
Even if the user's device is reachable and receiving push messages, push is not always shown or viewed. For such instances, Push Received will be called but not Push Notification View.
Following could be the reasons:
| Failure Code | Failure Reason | Description |
|---|---|---|
| Channel Opted Out | User opted out of channel | User has unsubscribed from the specific channel (e.g., "Marketing") |
| Unknown SDK Error | Unexpected SDK error | Technical issues during push processing/rendering. |
| Device Push Opted Out | Push disabled at device level | User turned off push permissions in device/app settings |
| User Push Opted Out | User Push Opted Out | User explicitly opted out through app interface |
| Timer Date Expired | Timer Date Expired | Timer-based push sent after the target date/time |
| Custom Push Render Failed | Custom push failed to render | Client-side custom rendering of notification failed |
| Invalid Push Content | Missing or invalid content | Required elements missing or corrupted |
For such failures happened on the client (device) end, SDK tracks Push Notification Failed event with above codes.

WebEngage Dashboard showing Push Notification Failed split by error_message
Analyze the occurrences of the event 'Push Notification Failed' to determine the impact on the WebEngage dashboard. For example, to determine the impact of Channel Opted Out, split the occurrences with error_message=Channel Opted Out. See the image reference above.
Channel opt-outs are the most recorded reason for view failures reported across clients and may be smartly handled by introducing unique channels as per the doc.
Android OS
Doze Mode: If the phone is stationary and the screen is off, Android enters Doze. Normal priority messages are queued. Only High priority messages can "poke" the device to wake up. High priority messages delivery can also be impacted and converted to a normal priority if user does not interact with the previously sent notifications. Read here to dive deeper.
Battery Saver Mode
In power-saving modes, restrictions are more aggressive:
Background Data Blocking: Most apps are prevented from accessing the network entirely.
App Pausing: The OS may "pause" non-essential apps, which stops them from sending or receiving notifications until the user manually opens them.
Execution Limits: Even if a high-priority message reaches the device, the OS might prevent the app's onMessageReceived() code from running immediately to save CPU cycles.
To tackle with these problems, WebEngage has implemented a solution (Push amplification) which amplifies the push delivery where FCM fails to deliver via FCM service.
User behaviour
The frequency with which the user opens/interacts with the application also influences the delivery of push notifications. User activity indirectly activates OS-level power-saving mechanisms, which block or delay FCM messages.
App Standby Buckets
App StandBy buckets were introduced in Android P as a part of battery saving feature. By default, the buckets are allocated based on their most recent usage. So the app which has not been used since long can fall in the rare bucket.
However, the buckets can also be allocated by OS using Machine Learning based on the user behaviour. The app is put in the bucket based on the possibility of its usage in the given time frame. This means that if the user uses the application every day from 3pm-9pm, and rarely uses the app at night, the app will be put in frequent standby bucket during 3pm-9pm and will be moved to rare bucket at night.
So when the device is in the rare bucket and in doze mode, the app needs FCM high priority message to wake the device and render the notification. However, if the limit defined by the OS for the high priority message is reached, any further high priority messages will be deprioritised to a normal priority and will not be able to wake the device for rendering the notification. These limits has changed from Android 13 and now controlled by OS. Read more here and here.
For example: This limit is set per day which means that if the app stays in the rare bucket, the app can only wake the device from doze mode for 5 times per day.
If the limit is reached and then the app is moved to any other bucket with lower restrictions, and then moved back to the rare bucket, the deprioritisation will continue happening. This means that event if the bucket changes any time during the day, as long as the bucket at the time of receiving the FCM is rare, the message will be deprioritised to normal priority. (Official google doc)
Live analysis of recency effect on Push delivery
The following is an analysis of live data that highlights the influence of push delivery and impressions related to recent behaviour.
We plotted the push delivery and impressions against the App open in that time period. Let's understand from analogy: Push sent on 6th September has an delivery rate of ~95% for the users who were active in last 7 days and the percentage of delivery decreases with going beyond 7 days.
Device Manufacturer (OEM-Specific Aggressive Optimization)
Manufacturers like Samsung, Xiaomi (MIUI), and Huawei often implement custom battery optimizations that are stricter than standard Android.
Auto-start Blocks: These can prevent an app from waking up to process an FCM intent unless the user manually whitelists the app in settings.
More stricter App Stand By buckets logic: Every manufacturer can set their own criteria for how non-active apps are assigned to buckets.
Swiping the app away force-stops the application killing all the background services including FCM, resulting in no delivery. Only, opening application manually will result in push delivery.
FCM Registration Token Lifecycle & Expiry
FCM tokens (registration tokens) are how FCM uniquely identifies a device/app instance.
Token Validity
Tokens are long-lived but become stale if the device doesn’t connect to FCM for ~30 days. (Firebase)
After 270 days of inactivity, FCM marks tokens as expired/invalid and rejects send attempts. (Firebase)
Stale registration tokens are tokens associated with inactive devices that have not connected to FCM for over a month. As time passes, it becomes less and less likely for the device to ever connect to FCM again. Message sends and topic fanouts for these stale tokens are unlikely to ever be delivered.
This means:
After extended inactivity (≥ 270 days), a device is essentially no longer reachable with its old token.
Only when the user opens the app again and register token refreshes will it become reachable again.
Official guidance: “When stale tokens reach 270 days of inactivity, FCM considers them expired … once expired, FCM marks them as invalid and rejects sends.” (Firebase)
Message TTL
If TTL expires while the device is offline, the message is dropped by the FCM itself, resulting in failed delivery to the device.
For example:
If a user is on a flight for 12 hours:
If you set TTL = 2 hours, the message is deleted by FCM/APNs before the user lands.
If you set TTL = 24 hours, the message will "burst" onto the phone as soon as they turn off Airplane Mode.
Configure Queuing on WebEngage Dashboard to set TTL.
Updated about 4 hours ago