Entre una push «enviada» y una push que el usuario ve en pantalla hay cinco saltos, y tú solo mandas en el primero. Tu backend acepta el mensaje y se lo pasa a FCM. A partir de ahí el control es de otros: FCM lo encola, Apple o la capa del fabricante filtran la entrega, el sistema operativo despierta tu app o no según la batería, y la bandeja lo pinta salvo que el canal esté silenciado. Un informe que marca 100% de envíos casi nunca significa que el 100% de la gente lo haya visto.
Esto es lo que no cuentan los tutoriales de «integra Firebase Messaging en 10 minutos». Meter el SDK es media hora. Conseguir que las notificaciones lleguen de forma fiable en un Xiaomi, en un iPhone con la app cerrada y en un Android 13 que estrenó permiso de notificaciones se lleva el resto del proyecto. A eso vamos.
La cadena de entrega: dónde se pierde cada push
En una app Flutter normal, el camino es este:
- Tu backend llama a la API HTTP v1 de FCM (la antigua API legacy con server key se apagó en julio de 2024; si todavía la usas, migra ya).
- FCM enruta el mensaje. Para Android lo entrega su propio canal; para iOS se lo pasa a APNs, el servicio de Apple.
- El dispositivo lo recibe y el sistema operativo lo gestiona según su estado de energía y las restricciones del fabricante.
- Tu app, o el propio sistema, construye la notificación.
- La bandeja la muestra, si el canal de notificación no está silenciado.
Cada salto es un punto de pérdida. Los dos que más fallan, con diferencia, no están en tu código: son el fabricante del Android y el gestor de energía de iOS.
Android: el problema no es Doze, son los fabricantes
Todo el mundo culpa a Doze, el modo de ahorro que Android introdujo en la versión 6. Doze retrasa la entrega cuando el móvil lleva rato quieto, pero tiene una salida limpia: una push de prioridad alta (priority: high en el payload de FCM) despierta el dispositivo y se entrega igual. Para eso existe la prioridad alta, y por eso debes reservarla para lo que la merece, no para un recordatorio de marketing.
El peso real está en las capas de los fabricantes chinos. MIUI de Xiaomi, EMUI de Huawei, ColorOS de Oppo, FuntouchOS de Vivo y las optimizaciones de Samsung cierran procesos en segundo plano mucho más rápido que el Android de serie. Si tu app no está en la lista blanca de batería, pueden matarla a los pocos minutos y, con ella, cualquier entrega que dependa de que tu código se ejecute. El sitio dontkillmyapp.com documenta el comportamiento fabricante por fabricante, y conviene tenerlo a mano cuando un cliente te reporta que «en mi móvil no llegan».
Dos mitigaciones que funcionan:
- Envía notification messages, no data-only. Una notification message la pinta el sistema en la bandeja sin arrancar tu proceso, así que sobrevive aunque el fabricante haya matado la app. Una data-only necesita que tu handler corra, y ahí es donde el fabricante te gana.
- Cuando una función concreta exija entrega fiable de verdad, guía al usuario a quitarte la optimización de batería. Es fricción, así que resérvalo para apps donde una push perdida tiene coste real: una alerta médica o un aviso de reparto.
iOS: aquí decide el sistema, no tú
En iOS la entrega pasa siempre por APNs y el margen de maniobra es menor. La distinción que importa es entre una notificación con alert, la que se ve, y una push silenciosa con content-available, la que despierta tu app en segundo plano para trabajar sin mostrar nada.
Las silenciosas están limitadas por diseño. Apple regula su frecuencia y puede retrasarlas o descartarlas según la batería y el uso de la app. Son una pista de que hay algo nuevo, nada más. Si tu arquitectura depende de que una push silenciosa sincronice un dato crítico al momento, tienes un bug esperando a producción. La push avisa; la app lo confirma con un pull cuando se abre. Ese patrón de hint-and-pull es el que aguanta, y sale del mismo razonamiento que aplicamos al decidir entre un sync engine y una capa propia offline-first.
El permiso que se te olvida pedir bien
Desde Android 13 (API 33), las notificaciones necesitan permiso explícito en tiempo de ejecución, el POST_NOTIFICATIONS. Antes se daban por concedidas al instalar; ahora el usuario tiene que aceptar, como en iOS desde siempre. Una parte de tu base instalada puede tener las notificaciones denegadas sin que tú lo sepas.
El error más caro es pedir el permiso en el primer arranque, antes de que el usuario entienda qué le aporta. Un «no» en esa pantalla cuesta revertirlo: toca ir a ajustes del sistema. Pide el permiso en contexto, cuando entra en juego la función que lo necesita, y explica qué va a recibir. En iOS tienes además la autorización provisional, que entrega notificaciones al centro de notificaciones sin prompt y deja que el usuario las promocione si le interesan.
final messaging = FirebaseMessaging.instance;
// Pídelo en contexto, no al arrancar.
final settings = await messaging.requestPermission(
provisional: false, // true en iOS si quieres probar la vía silenciosa
);
if (settings.authorizationStatus == AuthorizationStatus.denied) {
// No insistas con otro prompt: no reaparece. Lleva a ajustes
// solo si el usuario pide activar la función.
}
Tokens: la fuga silenciosa que nadie monitoriza
Cada dispositivo tiene un token de FCM, y ese token caduca. Rota cuando se reinstala la app, se restaura en otro móvil o el propio FCM lo invalida. Si tu backend sigue enviando al token viejo, la push se pierde en silencio: FCM la acepta, no devuelve un error inmediato, y el usuario deja de recibir nada sin que nadie se entere.
Dos reglas evitan la fuga. La primera, escuchar la rotación y actualizar el backend siempre que cambie:
FirebaseMessaging.instance.onTokenRefresh.listen((token) {
enviarTokenAlBackend(token);
});
La segunda, al cerrar sesión, borrar el token del dispositivo y marcarlo como inválido en tu base de datos. Un token vivo asociado a una cuenta que ya nadie usa es cómo acaban las notificaciones en el móvil equivocado. Vigila también las respuestas de FCM: cuando te marca un token como no registrado, límpialo. Una tabla de tokens que solo crece y nunca se poda es una métrica de entrega que miente.
Notification o data message: cuándo cada una
| Notification message | Data message | |
|---|---|---|
| La pinta | El sistema | Tu código |
| App cerrada o matada | Se muestra igual | Depende de que tu handler corra |
| Personalización | Limitada | Total |
| Fiabilidad en OEM agresivos | Alta | Baja |
| Uso típico | Avisos que deben verse sí o sí | Payload para la app, sincronización ligera |
Para casi todo lo que el usuario debe ver, usa notification messages, con un bloque data adjunto si quieres llevarlo a la pantalla correcta vía deep link. Reserva las data-only para trabajo en segundo plano que puedas permitirte perder.
Cuándo no apoyarte en una push
Una push nunca es un canal garantizado, y lo repetimos con cada cliente que quiere montar una función crítica encima de notificaciones. Si una alerta tiene que llegar sí o sí (un aviso de salud, una operación de fintech que el usuario debe aprobar, un corte de servicio), la push es el primer intento y necesita un plan B detrás: un SMS, un correo, un estado visible dentro de la app la próxima vez que se abra.
Al revés también: no montes sincronización crítica sobre pushes silenciosas. Úsalas como señal y confirma con pull. El móvil del usuario no es tu servidor, y el sistema operativo tiene la última palabra sobre qué se ejecuta y cuándo.
Nada de esto hace inútiles las notificaciones. Bien hechas, con la prioridad correcta, el permiso pedido en su momento, los tokens limpios y la fiabilidad medida en serio en vez de confiar en el «enviado», son uno de los canales de reenganche más fuertes que tiene una app. Solo hay que tratarlas como lo que son: un servicio de mejor esfuerzo sobre el que las garantías las pones tú.
En Dribba llevamos quince años peleando esta capa en apps con millones de usuarios. Si tus notificaciones llegan a medias y no sabes por dónde se pierden, es justo el tipo de problema que desenredamos en un proyecto de desarrollo a medida.



