FCM says success but the notification never arrives
Why Android does not show FCM notifications when the app is closed or killed: message types, priority, Doze, permissions, stale tokens and OEM battery savers.
FCM answered 200 with a message id, and the phone stayed silent. That response only means Google accepted the message. Between FCM and the screen there are six places it can stop. Work through them in this order; the first two explain most cases.
1. A data-only message while the app is killed
FCM has two kinds of message:
- Notification messages have a
notificationblock. When your app is in the background or closed, Android’s system tray shows them without running any of your code. - Data messages have only a
datablock. Android always hands them to your app’sonMessageReceived. If your app is not running and cannot be started, nothing is shown.
If your app builds the notification itself from data, it works while the app is open and fails when it is closed. The fix is to include a notification block for anything the user must see:
{
"message": {
"token": "DEVICE_TOKEN",
"notification": { "title": "Sign in from Chrome?", "body": "Tap to approve" },
"data": { "challenge_id": "apr_51mz0c" },
"android": { "priority": "high", "notification": { "channel_id": "logins" } }
}
}
2. The notification permission on Android 13 and later
From Android 13 (API 33), apps must ask for POST_NOTIFICATIONS at run time. Without it, FCM still delivers and returns success, and Android silently drops the notification. Ask at a moment the user understands, like right after they turn on order updates, not on the first screen.
3. Priority and Doze
When a phone sits idle, Android’s Doze mode holds normal-priority messages until a maintenance window, which can be many minutes later. High-priority messages wake the device. Set android.priority to high for messages the user is waiting for. Do not make everything high priority: Android may lower the priority of apps whose high-priority messages do not result in a visible notification.
4. A channel the user muted
On Android 8 and later every notification goes through a channel. If you send to a channel id that does not exist, or the user turned the channel off, nothing appears. Create your channels at app start and keep them separate, for example logins and offers, so users can mute offers without missing logins.
5. A stale or wrong token
Tokens change when the app is reinstalled, its data is cleared, or it is restored on a new phone. A token from your development build will not work with your production Firebase project either. Send to a dead token and v1 returns UNREGISTERED; delete it. Refresh the token in your app with onNewToken and send the new one to your server every time.
6. The phone maker’s battery saver
Xiaomi, Redmi, Poco, Oppo, Realme, Vivo and OnePlus phones add their own battery management on top of Android. Swiping an app away from recents can act like a force stop, and Android does not deliver messages to a force-stopped app until the user opens it again. This is the most common reason pushes fail in India, where these brands sell most of the phones. The fixes are in push notifications on Xiaomi, Oppo, Vivo and Realme.
A quick test that separates the causes
- Send a notification message to one token from the Firebase console’s test send, with the app in the background, not swiped away.
- If that shows, your message type or channel is the problem.
- If it does not, check the permission, then try the same test on a Pixel or Samsung. If it shows there and not on a Xiaomi, it is the battery saver.
What SignOps does about it
SignOps Push sends notification messages with high priority only where the user expects them, keeps separate channels for approvals and offers, removes tokens FCM reports as dead, and gives you a delivery log per device. For Approve, the SDK also fetches pending requests when the app opens, so an approval held by a battery saver still reaches the user.
Sources: Firebase: receive messages in an Android app, Android: notification runtime permission, Android: optimize for Doze and App Standby.