Pérdida de Mensajes en Redis Pub/Sub: Causas y Alternativas Confiables

Descubra por qué Redis Pub/Sub pierde mensajes durante desconexiones de red o consumidores lentos y explore patrones como Redis Streams y colas basadas en listas para entrega garantizada.

Pérdida de Mensajes en Redis Pub/Sub: Causas y Alternativas Confiables

Recuerdo la primera vez que Redis Pub/Sub me falló. Era tarde, alrededor de las 11 PM, y nuestro sistema de notificaciones comenzó a perder mensajes. No todos — solo los suficientes para que los usuarios lo notaran antes que nosotros. El ingeniero de guardia (yo, desafortunadamente) pasó dos horas revisando registros de aplicaciones antes de que la verdad obvia saliera a la luz: Redis Pub/Sub no encola nada. No es un broker de mensajes. Es una manguera de incendios, y si no estás parado directamente frente a ella con la boca abierta, te vas a perder algo.

Eso es lo que nadie te dice cuando usas Redis Pub/Sub por primera vez. Está en la documentación, técnicamente, pero es fácil pasarlo por alto cuando estás emocionado por lo simple que es la API. Publicas en un extremo, te suscribes en el otro, y funciona. Hasta que no funciona.

La realidad de "disparar y olvidar"

Redis Pub/Sub opera bajo un principio brutalmente simple: cuando publicas un mensaje, Redis lo envía a cada suscriptor conectado en ese canal en ese momento exacto. Si un suscriptor no está conectado, o si está conectado pero no puede seguir el ritmo, el mensaje se evapora. No hay capa de persistencia, ni mecanismo de confirmación, ni cola de mensajes fallidos. El mensaje existe solo en tránsito.

Déjame darte un ejemplo concreto. Supón que tienes un servicio que publica actualizaciones de estado de pedidos, y otro servicio que se suscribe para enviar correos de confirmación. Bajo carga normal, todo funciona bien. Luego tu servicio de correo tiene un problema — tal vez el relay SMTP es lento, tal vez hay una pausa por recolección de basura. Durante ese problema, Redis sigue enviando mensajes. El búfer TCP del suscriptor se llena. Eventualmente, la conexión se cae. Cuando el suscriptor se reconecta, retoma desde ahora, no desde donde se quedó. Cada mensaje publicado durante la ventana de desconexión se pierde.

He medido esto en la práctica con una configuración de prueba simple: un publicador enviando 10,000 mensajes por segundo, y un suscriptor que ocasionalmente se bloquea por 50 milisegundos. Incluso con una breve pausa, perderás docenas de mensajes. El suscriptor nunca sabe que fueron enviados. El publicador nunca sabe que se perdieron. Redis está perfectamente contento — hizo exactamente lo que fue diseñado para hacer.

Qué causa realmente la pérdida de mensajes

Hay tres escenarios principales donde Pub/Sub pierde mensajes, y vale la pena entenderlos porque se manifestarán de diferentes maneras.

Inestabilidad de red es la más obvia. Cualquier partición de red temporal entre el suscriptor y Redis rompe la conexión. Redis detecta esto mediante el tiempo de espera del cliente (60 segundos por defecto, pero podrías tenerlo configurado más bajo). Durante esa ventana, todos los mensajes publicados se pierden para ese suscriptor. Otros suscriptores podrían recibirlos bien, lo que hace la depuración más divertida — verás estado inconsistente entre servicios y te preguntarás si te estás volviendo loco.

Consumidores lentos son más insidiosos porque la conexión permanece abierta. Redis usa un modelo push, lo que significa que escribe en los sockets de los suscriptores tan rápido como los publicadores producen. Si un suscriptor no puede procesar mensajes lo suficientemente rápido, el búfer de recepción TCP del kernel se llena. Una vez que ese búfer está lleno, Redis no puede escribir más datos, y la conexión eventualmente falla. El suscriptor podría ni siquiera notar que está atrasado hasta que ocurre la desconexión.

He visto esto con suscriptores que hacen escrituras síncronas en la base de datos por cada mensaje. A bajo volumen, está bien. En pico, la base de datos se convierte en el cuello de botella, el suscriptor se atrasa, y los mensajes se acumulan en el búfer TCP. Cuando ese búfer se desborda, la conexión se reinicia, y el suscriptor pierde todo lo que no había leído del socket.

Desconexiones de clientes durante despliegues o reinicios son la tercera gran categoría. Si estás haciendo despliegues continuos y una instancia de suscriptor se cae, pierde todo lo publicado durante su ausencia. No hay un mecanismo de "ponme al día". Cuando vuelve a estar en línea, comienza desde cero.

Algo que me sorprendió: incluso un apagado limpio no ayuda. Si tu suscriptor se da de baja correctamente antes de salir, aún pierde mensajes publicados entre la baja y cuando vuelve. La baja es instantánea — no hay una opción de "guarda mis mensajes por un minuto".

Cuándo Pub/Sub está realmente bien

No quiero hacer parecer que Redis Pub/Sub es inútil. Es excelente para casos de uso específicos, y todavía lo uso regularmente. La clave es entender cuáles son esos casos de uso.

Las notificaciones en tiempo real donde la pérdida ocasional es aceptable funcionan perfectamente. Piensa en marcadores de deportes en vivo, tickers de acciones, o indicadores de escritura en una app de chat. Si un usuario pierde una actualización de marcador, la siguiente llegará en unos segundos de todos modos. Los datos tienen una vida útil corta y ningún requisito de durabilidad.

El descubrimiento de servicios y la difusión de configuración son otro punto ideal. Cuando cambias un feature flag y lo publicas a todas las instancias de la aplicación, está bien si una instancia que se está reiniciando pierde la actualización — recogerá el estado actual cuando vuelva a estar en línea o en la próxima actualización periódica.

También he usado Pub/Sub con éxito para invalidación de caché en múltiples servidores de aplicaciones. Publica una clave de caché para invalidar, y cada servidor limpia su caché local. Si un servidor pierde el mensaje, el peor caso es que sirva datos obsoletos hasta que la entrada de caché expire naturalmente. No es ideal, pero tampoco catastrófico.

El hilo común aquí: Pub/Sub funciona cuando los mensajes son efímeros por naturaleza, cuando la pérdida es recuperable a través de otros mecanismos, y cuando no necesitas garantías de orden o entrega exactamente una vez.

Redis Streams: la alternativa integrada

Redis Streams, introducido en Redis 5.0, es a lo que recurro ahora cuando necesito entrega confiable de mensajes. No es Pub/Sub con persistencia añadida — es un modelo fundamentalmente diferente, más cercano a un log distribuido como Kafka que a un mecanismo de difusión.

Con Streams, los mensajes se añaden a un log y permanecen allí hasta que se confirman explícitamente. Los consumidores pueden desconectarse, reiniciarse, atrasarse, y aún así ponerse al día. El stream retiene mensajes basado en una longitud máxima o un período de retención, por lo que controlas cuánto historial mantener.

Así es como difiere el modelo mental. En Pub/Sub, te suscribes a un canal y los mensajes fluyen hacia ti. En Streams, extraes mensajes a tu propio ritmo. Un grupo de consumidores rastrea qué mensajes ha confirmado cada consumidor, por lo que puedes tener múltiples consumidores leyendo del mismo stream sin duplicación (o con duplicación intencional, si quieres distribución).

Una configuración básica de Streams se ve así:

XADD orders * status confirmed order_id 12345

Eso añade un mensaje al stream orders. El * le dice a Redis que genere un ID automáticamente. Luego tu consumidor lee con:

XREADGROUP GROUP email-processor worker-1 COUNT 10 STREAMS orders >

El > significa "dame mensajes que no han sido entregados a ningún consumidor en este grupo todavía". Después de procesar, el consumidor confirma:

XACK orders email-processor <message-id>

Si el consumidor se cae antes de confirmar, el mensaje queda pendiente. Otro consumidor en el grupo puede reclamarlo con XCLAIM después de un tiempo de espera. Este es el mecanismo de confirmación y reentrega que Pub/Sub carece por completo.

El modelo de grupo de consumidores en la práctica

Los grupos de consumidores son lo que hace que Streams sea genuinamente útil para procesamiento confiable. Cada grupo mantiene su propia posición en el stream, por lo que puedes tener un grupo para notificaciones por correo, otro para análisis, y otro para registro de auditoría — todos leyendo el mismo stream de forma independiente.

Dentro de un grupo, los mensajes se distribuyen entre los consumidores. Esto te da escalabilidad horizontal: añade más instancias de consumidor, y compartirán la carga. Si una instancia muere, sus mensajes pendientes se vuelven disponibles para que otras instancias los reclamen.

He encontrado que la lista de entradas pendientes es invaluable para monitoreo. Puedes ejecutar XPENDING para ver qué mensajes no han sido confirmados y cuánto tiempo han estado pendientes. Esto revela consumidores lentos de inmediato — mucho mejor que descubrir pérdida de mensajes días después a través de quejas de usuarios.

Un inconveniente con Streams: los IDs de mensajes son marcas de tiempo monótonamente crecientes, lo que significa que no puedes insertar mensajes fuera de orden fácilmente. Si necesitas orden estricto dentro de un stream, esto es en realidad una característica. Si necesitas priorizar ciertos mensajes, necesitarás múltiples streams o un enfoque diferente.

Colas basadas en listas para necesidades más simples

Antes de que existieran Streams, el patrón estándar para mensajería confiable con Redis eran colas basadas en listas con pops bloqueantes. Este patrón sigue siendo perfectamente viable, especialmente si estás en una versión anterior de Redis o quieres algo extremadamente simple.

La idea es directa: los productores hacen LPUSH o RPUSH de mensajes en una lista, y los consumidores hacen BLPOP o BRPOP para bloquearse hasta que llegue un mensaje. El pop bloqueante es crucial — sin él, estarías haciendo polling, lo que desperdicia CPU y añade latencia.

La confiabilidad viene de una lista secundaria de "procesamiento". El consumidor mueve atómicamente un mensaje de la cola pendiente a una cola de procesamiento usando BRPOPLPUSH (o LMOVE en Redis 6.2+). Después de procesar, elimina el mensaje de la cola de procesamiento. Si el consumidor se cae, la cola de procesamiento retiene el mensaje, y un proceso monitor puede mover elementos obsoletos de vuelta a la cola pendiente.

He construido este patrón varias veces, y funciona, pero es más código del que esperarías. Necesitas manejar tiempos de espera, decidir cuánto tiempo puede estar un mensaje en la cola de procesamiento antes de considerarlo abandonado, y lidiar con casos extremos de procesamiento duplicado. Streams esencialmente formaliza todo esto, por lo que me he alejado en su mayoría de las colas de listas hechas a mano.

El único lugar donde todavía uso colas basadas en listas es para colas de trabajo donde el orden de procesamiento no importa y quiero la implementación más simple posible. A veces una lista y un bucle BLPOP es todo lo que necesitas, y añadir Streams sería sobrediseño.

Pub/Sub fragmentado en Redis 7

Redis 7 introdujo Pub/Sub fragmentado, que vale la pena mencionar porque resuelve un problema diferente al de la pérdida de mensajes. Con Pub/Sub regular, cada mensaje se difunde a cada nodo en un clúster, incluso si ningún suscriptor en un nodo dado se preocupa por ese canal. Esto desperdicia ancho de banda de interconexión del clúster.

Pub/Sub fragmentado vincula canales a slots específicos del clúster, por lo que los mensajes solo se propagan a nodos que realmente tienen suscriptores para ese canal. Es una optimización de rendimiento, no una característica de confiabilidad. Aún perderás mensajes en desconexión. Pero si estás ejecutando Pub/Sub a escala en un entorno en clúster, vale la pena saberlo.

Tomando la decisión: Pub/Sub vs Streams vs listas

Después de vivir con estos patrones durante años, mi proceso de decisión se ha simplificado a unas pocas preguntas.

Primero: ¿puedes tolerar la pérdida de mensajes? Si sí, y si los datos son efímeros, Pub/Sub probablemente esté bien. Obtendrás la latencia más baja y el modelo operativo más simple.

Segundo: ¿necesitas persistencia y reproducción de mensajes? Si sí, Streams es la respuesta. La capacidad de reprocesar mensajes después de una corrección de error en el consumidor me ha salvado más de una vez. Con Pub/Sub, si tu consumidor tuvo un error que causó que manejara mal los mensajes durante una hora, esos mensajes se han ido para siempre. Con Streams, puedes restablecer la posición del grupo de consumidores y reproducirlos.

Tercero: ¿necesitas múltiples grupos de consumidores independientes leyendo los mismos datos? Streams maneja esto de forma nativa. Con Pub/Sub, cada suscriptor recibe cada mensaje, lo que podría ser lo que quieres, pero no hay forma de tener diferentes grupos de suscriptores manteniendo posiciones independientes.

Cuarto: ¿cuál es tu versión de Redis? Si estás atascado en algo anterior a 5.0, Streams no está disponible, y estás viendo colas basadas en listas o un broker de mensajes externo. He estado en esta situación, y honestamente, si necesitas mensajería confiable y no puedes usar Streams, consideraría si Redis es la herramienta adecuada en absoluto. RabbitMQ o NATS podrían ser mejores opciones.

El lado operativo del que nadie habla

Aquí hay algo que aprendí por las malas: monitorear Pub/Sub es engañosamente difícil. Puedes monitorear conteos de conexiones y suscripciones de canales con PUBSUB NUMSUB, pero no puedes ver cuántos mensajes se están perdiendo. No hay una métrica para "mensajes publicados pero no recibidos" porque Redis no rastrea eso.

Con Streams, obtienes visibilidad. XINFO GROUPS te muestra el retraso del consumidor. XPENDING te muestra mensajes no confirmados. Puedes configurar alertas cuando el retraso excede un umbral. Esta visibilidad operativa por sí sola ha hecho que Streams valga la pena para mí.

La gestión de memoria es otra consideración. Los mensajes de Pub/Sub existen solo en memoria y solo mientras están en tránsito, por lo que el uso de memoria está limitado por tu tasa de publicación y velocidad del consumidor. Streams almacena mensajes hasta que se recortan, por lo que necesitas pensar en políticas de retención. Normalmente establezco una longitud máxima de stream (MAXLEN) basada en el rendimiento esperado y la memoria disponible, y monitoreo la longitud del stream para detectar acumulaciones inesperadas.

Lo que realmente hago ahora

Hoy en día, por defecto uso Redis Streams para cualquier caso de uso de mensajería que requiera confiabilidad. La API es ligeramente más compleja que Pub/Sub, pero no mucho, y las garantías de confiabilidad valen la pena. Mantengo Pub/Sub para lo efímero — invalidación de caché, presencia en tiempo real, ese tipo de cosas.

Para mensajería particularmente crítica (procesamiento de pagos, cumplimiento de pedidos), me he alejado de Redis por completo y uso brokers de mensajes dedicados. Redis es fantástico en muchas cosas, pero no está optimizado para persistencia basada en disco de colas de mensajes de alto volumen. Si necesitas que los mensajes sobrevivan a un reinicio completo de Redis con pérdida cero, necesitas configurar persistencia AOF con appendfsync always, lo que perjudica el rendimiento de escritura. En ese punto, algo como Kafka o Pulsar tiene más sentido.

Pero para el vasto término medio — donde la pérdida de mensajes sería molesta o costosa pero no catastrófica, y donde quieres permanecer dentro del ecosistema Redis que ya conoces — Streams alcanza un punto óptimo. Ha sido lo suficientemente confiable para mí en producción, y la simplicidad operativa de no introducir un nuevo componente de infraestructura tiene un valor real.

El error original que cometí con Pub/Sub no fue realmente sobre la tecnología. Fue sobre no leer la letra pequeña, sobre asumir que "mensajería" implicaba "garantías de entrega de mensajes". Redis Pub/Sub no hace tales garantías, y no pretende hacerlo. Una vez que entiendes eso, puedes usarlo apropiadamente y recurrir a Streams cuando necesites más.