fcm android ios debugging

FCM says delivered, the phone shows nothing: the data-only message

FCM has two message shapes and they behave completely differently. One of them is drawn by the system; the other is not, and reports success either way.

· 4 min read · Notibase

You send this and get back a message id:

{
  "message": {
    "token": "…",
    "data": { "title": "Your order shipped", "body": "Track it in the app." }
  }
}

Nothing appears. Not late, not sometimes — nothing, and the API told you it worked.

The rule

FCM has two message shapes and they behave completely differently.

A message with a notification object is a notification message. When your app is in the background or not running, the system draws it. Your code is not involved. This is what most people mean when they say "send a push".

A message with only data is a data message. The system draws nothing. It hands the payload to your app and expects your app to construct a notification, or do something else entirely. If your app is not in a position to receive it, nothing happens at all.

FCM's response tells you it accepted the message for delivery. That is true. Whether anything was ever going to be visible is a question it does not answer, because from FCM's point of view a data message that reached a running app did exactly what it was asked to do.

What "not in a position to receive it" means

Android. A data message wakes your app's onMessageReceived. That works while the app is in the background, but not after a force-stop: an app the user swiped away from recents is usually fine; an app force-stopped from Settings receives nothing until it is opened again. Some manufacturers — the aggressive battery-optimisation ones — treat their own cleanup as a force-stop, which is why "it works on my Pixel" is not evidence.

iOS. A data message needs content-available: 1 in the APNs half, and your app needs the Remote notifications background mode. Even then iOS decides when and whether to wake you, based on battery, how often the user opens the app, and its own scheduling. Apple is explicit that these are best-effort and may be throttled or dropped. Building a visible notification out of a silent push on iOS is not a supported pattern; the visible thing has to be an alert.

So on iOS a data-only message with no content-available does nothing at all, and with content-available does something unpredictable. Neither is what you wanted.

The shape you probably want

Send a notification block for the visible part, and data alongside it for whatever your app needs when the notification is tapped:

{
  "message": {
    "token": "…",
    "notification": { "title": "Your order shipped", "body": "Track it in the app." },
    "android": { "notification": { "channel_id": "orders" } },
    "apns": { "payload": { "aps": { "sound": "default" } } },
    "data": { "order_id": "A-1183" }
  }
}

Both halves arrive. The system draws the notification whether or not your app is running, and data is there in the tap handler.

When a data-only message is right

It is a real feature and not a trap, as long as you are using it for what it is for:

  • Silent sync. Wake the app to fetch new content so the UI is current when it opens. Nothing visible, by design.
  • A notification the server cannot compose. Anything needing on-device state — a decrypted message body, a count only the client knows, a string in a language the server was never told about.
  • Cancelling or updating a notification already on screen.

In all three the invisibility is the point. The bug is only a bug when you expected the system to draw something.

One more thing that will bite

data values must all be strings. FCM rejects the entire message if one is a number, a boolean or a nested object — a validation error, not a silent drop, so at least this one tells you. But it is easy to introduce months later when somebody adds "order_id": 1183 to a payload that has always worked, and the error text does not name the offending key.

"data": { "order_id": "1183", "items": "[\"a\",\"b\"]" }

Stringify before you send, and parse on the other side.

Checking without sending

You can settle all of this from the payload alone. We built a push payload validator that says which shape a message is and what each platform will do with it — including the data-only case, the string rule, and the Android channel that has to exist before it arrives. It runs entirely in your browser, nothing is uploaded, and there is no account.


We build Notibase — push, email, SMS, in-app messages and an in-app inbox to one audience through one API, on your own Firebase and APNs credentials. See also why an Android channel silently drops notifications, and our message content reference for every field a message can carry and which platforms honour it.