Push con FCM: token, servidor, carga útil y entrega
Una notificación push atraviesa cinco fronteras antes de llegar al usuario: tu servidor, la infraestructura de Firebase, el canal persistente del sistema operativo, las políticas de energía del dispositivo y por fin tu proceso. Esta lección recorre esa cadena completa. Explica qué es realmente el token de registro y por qué su ciclo de vida es la fuente de fallos silenciosos más común, cómo autentica un servidor sus envíos y por qué la clave heredada desapareció, la diferencia decisiva entre una carga útil de notificación y una de datos y cómo determina qué código tuyo se ejecuta, el comportamiento divergente en primer y segundo plano, y las restricciones reales de entrega: prioridades, tiempo de vida, contracción de mensajes, cuotas y modos de ahorro de energía.
El push es la única forma que tiene tu aplicación de existir cuando nadie la está mirando, y esa capacidad tiene un precio arquitectónico que casi todos los equipos descubren tarde. Un mensaje que va de tu servidor a un teléfono no viaja por una conexión tuya: viaja por un canal único que el sistema operativo mantiene abierto para todas las aplicaciones a la vez, precisamente para no tener que mantener cientos de conexiones simultáneas que devorarían la batería. Ese canal compartido es lo que hace posible el push en un dispositivo móvil, y también lo que explica todas sus restricciones. Tú no controlas la conexión, no controlas cuándo se entrega, no controlas si se entrega, y ni siquiera controlas qué parte de tu código se ejecuta al llegar el mensaje, porque eso depende de un detalle de la carga útil que la mayoría de la gente rellena por costumbre. Entender el push consiste en aceptar que estás publicando una intención de entrega en un sistema de mejor esfuerzo, y diseñar en consecuencia.
- Describir el recorrido completo de un mensaje y qué garantiza y qué no cada tramo.
- Gestionar el ciclo de vida del token de registro sin dejar destinatarios fantasma en tu servidor.
- Elegir con criterio entre carga útil de notificación, de datos o mixta según el código que quieras ejecutar.
- Anticipar las restricciones reales de entrega: prioridad, tiempo de vida, contracción, cuotas y ahorro de energía.
El token: identidad frágil de un destinatario
El token de registro es una cadena opaca que identifica de forma única la pareja formada por una instalación concreta de tu aplicación y un dispositivo concreto. No identifica al usuario, no identifica la cuenta y no sobrevive necesariamente a nada. Esa distinción es el origen de la mayoría de errores de este tema, porque los servidores suelen almacenarlo como si fuera un identificador estable de persona.
El token cambia o se invalida cuando la aplicación se reinstala, cuando se restauran los datos en otro dispositivo, cuando se borran los datos de la app, cuando el usuario desinstala, y de forma periódica por decisión de la infraestructura. Tu única obligación es escuchar la rotación y enviar el valor nuevo, pero eso no basta: hay un caso en el que la rotación no te llega nunca, y es el de la desinstalación. El servidor de Firebase te informará del token inválido en la respuesta del envío, y si no lees esa respuesta acumularás destinatarios muertos indefinidamente, degradando tus métricas y tus cuotas sin ninguna señal visible.
class MensajeriaApp : FirebaseMessagingService() {
override fun onNewToken(token: String) {
// Se invoca en rotacion y en primera obtencion, no en cada arranque
registrarEnServidor(token, usuarioActual())
}
override fun onMessageReceived(mensaje: RemoteMessage) {
// Ver la seccion de cargas utiles
}
}
Hay dos reglas operativas que eliminan familias enteras de fallos. La primera es asociar el token a la sesión y no a la instalación: al cerrar sesión hay que borrar el token del servidor y, si el dispositivo es compartido, eliminarlo también localmente para que el usuario siguiente no reciba avisos del anterior. Ese fallo, cuando ocurre, es una fuga de información entre cuentas y no una molestia menor.
La segunda es tratar el registro como una operación que puede fallar y debe reintentarse. La rotación puede llegar mientras el dispositivo está sin red o mientras tu sesión ha caducado, y si esa única llamada se pierde el usuario queda mudo para siempre sin que nadie lo detecte. Delegar el envío a un trabajo persistente con reintentos, en lugar de a una corrutina que muere con el proceso, es la diferencia entre un sistema fiable y uno que pierde silenciosamente un porcentaje de su base cada mes.
Almacena junto a cada token la fecha del último registro correcto y la del último envío con éxito. Un token que lleva meses sin refrescarse y acumula errores de destinatario no registrado es basura que infla tus cifras de alcance y consume cuota. La higiene mínima consiste en purgar por respuesta de error y caducar por antigüedad, y sin ella cualquier medición de la tasa de entrega es ficción.
Del servidor a la infraestructura
El envío se realiza contra la interfaz de programación de la nube de Firebase, autenticada con credenciales de cuenta de servicio y testigos de acceso de vida corta obtenidos mediante el protocolo de autorización habitual. La clave de servidor heredada, aquella cadena estática que se pegaba en una cabecera y se filtraba en repositorios con regularidad deprimente, ya no está disponible: fue retirada precisamente porque un secreto permanente y omnipotente es un modelo de seguridad indefendible.
De esa arquitectura se sigue una regla que no admite excepciones: los envíos se hacen desde tu servidor, jamás desde el cliente. Una aplicación que llevase credenciales capaces de enviar mensajes podría enviárselos a cualquier usuario del sistema, y esas credenciales son extraíbles de cualquier binario con esfuerzo trivial.
Existen tres formas de dirigir un mensaje y sirven para cosas distintas. El envío a token individual es preciso y es el único adecuado para contenido personal. El envío a tema permite que los dispositivos se suscriban por interés y que un único envío alcance a todos los suscritos, con la contrapartida de que la suscripción la controla el cliente y por tanto un tema nunca debe transportar información sensible. El envío a grupo de dispositivos cubre el caso de una persona con varios aparatos.
Token individual
Máxima precisión y única opción válida para contenido personal. Exige mantener el inventario de tokens al día.
Tema
Difusión por interés con un solo envío. La suscripción es del cliente, así que nunca contenido sensible.
Condición de temas
Expresiones lógicas sobre varias suscripciones para segmentar sin mantener listas en tu servidor.
Grupo de dispositivos
Un usuario con varios aparatos tratado como un destinatario único, con contracción coherente entre ellos.
La carga útil decide qué código se ejecuta
Este es el punto que más comportamiento inesperado produce y el que casi nunca se explica bien. Un mensaje puede llevar un bloque de notificación, un bloque de datos, o los dos. La elección no es estilística: determina si el sistema construye la notificación por su cuenta o si la construye tu código, y por tanto si tu código llega siquiera a ejecutarse.
Con un bloque de notificación y la aplicación en segundo plano, la biblioteca construye y muestra la notificación sin invocar tu método de recepción. Tu lógica no corre. No puedes traducir, ni personalizar, ni consultar el estado local, ni decidir no mostrarla. Con la aplicación en primer plano, en cambio, sí se invoca tu método y no se muestra nada automáticamente. El resultado es una aplicación que se comporta de dos formas distintas según un estado que tú no controlas, y esa asimetría es exactamente el origen del clásico funciona en mi máquina de este tema.
Con un bloque de datos exclusivamente, tu método de recepción se invoca siempre, en primer y en segundo plano, y toda la construcción es tuya. Es la única configuración que da control completo, y su precio es que ahora eres responsable de todo, incluido de comprobar el permiso y el canal antes de mostrar nada.
{
"message": {
"token": "TOKEN_DEL_DISPOSITIVO",
"data": {
"tipo": "mensaje_directo",
"conversacion": "881",
"autor": "Ana",
"cuerpo": "Subo la propuesta esta tarde"
},
"android": {
"priority": "high",
"ttl": "3600s",
"collapse_key": "conv_881"
}
}
}
override fun onMessageReceived(mensaje: RemoteMessage) {
val datos = mensaje.data
if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) return
when (datos["tipo"]) {
"mensaje_directo" -> {
almacenarLocalmente(datos) // primero la verdad duradera
if (conversacionAbierta(datos["conversacion"])) return
mostrarComoConversacion(datos)
}
"sincronizar" -> encolarTrabajoDeSincronizacion()
else -> Log.w("push", "tipo desconocido")
}
}
El mensaje mixto, con los dos bloques, es la peor opción de las tres y aparece por acumulación histórica más que por decisión: en segundo plano el sistema muestra el bloque de notificación y entrega los datos únicamente en los extras de la intención de apertura, de modo que tu lógica solo corre si el usuario toca. Cualquier procesamiento que dependa de esos datos se ejecutará tarde o nunca.
flowchart TD
A[Tu servidor envia el mensaje] --> B[Infraestructura de Firebase]
B --> C{Tipo de carga util}
C -->|solo notificacion| D{Estado de la app}
D -->|segundo plano| E[El sistema la muestra y tu codigo no corre]
D -->|primer plano| F[Se invoca tu metodo y no se muestra nada]
C -->|solo datos| G[Se invoca tu metodo siempre]
G --> H[Guardas en local y decides si mostrar]
H --> I[Notificacion construida por ti]
style E fill:#f9e2af,color:#11111b
style I fill:#a6e3a1,color:#11111bLo que la entrega no garantiza
El push es un servicio de mejor esfuerzo y conviene enumerar sin eufemismos lo que puede ocurrirle a un mensaje. Puede llegar con retraso arbitrario, puede llegar desordenado respecto a otro enviado antes, puede llegar duplicado, y puede no llegar nunca. Cualquier diseño que suponga entrega exacta y ordenada está construido sobre arena.
La prioridad es la palanca principal y tiene dos valores efectivos. La prioridad alta despierta el dispositivo aunque esté en reposo profundo y entrega el mensaje de inmediato; está reservada a contenido que el usuario percibe como inmediato, como un mensaje de otra persona o una llamada, y su uso indiscriminado se penaliza con una reducción de la tasa de entrega para toda la aplicación. La prioridad normal se agrupa con otras entregas en las ventanas de mantenimiento del sistema y puede acumular retrasos de minutos u horas en un dispositivo en reposo.
El tiempo de vida define cuánto conserva la infraestructura un mensaje no entregado. Un valor bajo es lo correcto para contenido perecedero, porque un aviso de que la cena está lista entregado a las tres de la mañana es peor que ningún aviso. Un valor de cero significa entrega inmediata o descarte.
La clave de contracción resuelve el problema del dispositivo que estuvo desconectado. Los mensajes que comparten clave se colapsan y solo se entrega el último, lo que evita que un teléfono que vuelve de un vuelo reciba cuarenta actualizaciones de la misma partida. Se aplica únicamente a la prioridad normal.
Sobre todo lo anterior actúan las políticas de energía del dispositivo, que son la variable más olvidada. El modo de reposo profundo, los cubos de inactividad y las restricciones de aplicaciones en segundo plano recortan lo que puede ocurrir al recibir un mensaje; y muchos fabricantes añaden capas propias, más agresivas y no documentadas, que llegan a suspender la mensajería de aplicaciones que el usuario no abre a menudo. Un push que funciona impecablemente en un dispositivo de referencia puede tener una tasa de entrega notablemente peor en teléfonos de gran distribución, y ese sesgo no aparece en ninguna prueba de laboratorio.
La consecuencia arquitectónica de todo esto es una sola y merece formularse como regla: el push es una señal, no un transporte de datos. El mensaje debe decir algo cambió aquí, y tu cliente debe ir a buscar la verdad a tu servidor. Un diseño que transporta el contenido dentro de la carga útil y lo trata como fuente de verdad producirá estados divergentes en cuanto un mensaje se pierda, se duplique o llegue fuera de orden, que son las tres cosas que el sistema explícitamente permite que ocurran.
Detrás del push hay una lección de sistemas distribuidos que conviene extraer con cuidado porque se aplica mucho más allá de las notificaciones. Todo el que aprende a usar esta tecnología pasa por la misma tentación inicial: como el mensaje admite datos arbitrarios, parece natural meter dentro el contenido completo y tratar la llegada como el evento que actualiza el estado del cliente. Ese diseño funciona en desarrollo con una fidelidad engañosa, porque en un emulador conectado y despierto la entrega es inmediata, ordenada y exacta, y falla en producción de formas que resultan desconcertantes hasta que se identifica su origen común: se ha construido un sistema de replicación de estado sobre un canal que no promete ni orden, ni unicidad, ni entrega. La corrección conceptual es la misma que se descubrió hace décadas en el diseño de protocolos y que se reformula en cada generación de tecnología: separar la señalización del transporte de estado. Un aviso de que algo cambió es idempotente por naturaleza, tolera duplicados sin consecuencias, tolera el desorden porque solo importa el último, y tolera la pérdida porque cualquier sincronización posterior lo subsume. Un fragmento de estado transportado por el mismo canal no tolera nada de eso: duplicarlo corrompe, desordenarlo revierte y perderlo deja un agujero permanente que ninguna reconexión repara. La regla que se desprende es sencilla de enunciar y sorprendentemente difícil de sostener bajo presión de entrega, porque el atajo siempre parece más barato: el canal no fiable transporta la invitación a sincronizar, y el canal fiable, que es tu petición autenticada al servidor, transporta la verdad. Quien interioriza esa separación obtiene además una ventaja inesperada, y es que su cliente se vuelve inmune a la clase entera de problemas de entrega. Ya no importa si el mensaje llegó tarde, dos veces o fuera de orden, porque la única consecuencia de recibirlo es preguntar; y preguntar de más nunca ha roto un sistema bien diseñado.
- Instrumenta el ciclo de vida del token registrando cada rotación y cada error de destinatario no registrado, y mide qué porcentaje de tu inventario es basura.
- Convierte un mensaje mixto de tu sistema en uno de datos exclusivamente y demuestra que tu código ahora se ejecuta también en segundo plano.
- Envía la misma notificación con prioridad alta y con prioridad normal a un dispositivo en reposo profundo y cronometra la diferencia de entrega.
- Diseña la clave de contracción y el tiempo de vida de tus tres tipos de mensaje más frecuentes, justificando cada valor con la caducidad real del contenido.
- Refactoriza el mensaje que más datos transporta para que sea solo una señal, y comprueba que el estado del cliente sigue siendo correcto si lo entregas duplicado y fuera de orden.