PostgreSQL LISTEN/NOTIFY puede impulsar dashboards y alertas en vivo con configuración mínima. Aprende dónde encaja, sus límites y cuándo añadir un broker.

“Actualizaciones en vivo” en la UI de un producto suele significar que la pantalla cambia poco después de que ocurre algo, sin que el usuario tenga que refrescar. Un número se incrementa en un dashboard, aparece una insignia roja en una bandeja de entrada, un admin ve un pedido nuevo o aparece un toast que dice “Build finished” o “Payment failed”. La clave es el tiempo: se siente instantáneo, aunque en realidad sea un segundo o dos.
Muchos equipos empiezan con polling: el navegador pregunta al servidor “¿hay algo nuevo?” cada pocos segundos. El polling funciona, pero tiene dos inconvenientes comunes.
Primero, se siente lento porque los usuarios solo ven los cambios en la siguiente consulta programada.
Segundo, puede volverse caro porque haces comprobaciones repetidas incluso cuando no cambia nada. Multiplica eso por miles de usuarios y se convierte en ruido.
PostgreSQL LISTEN/NOTIFY existe para un caso más simple: “avísame cuando algo cambie”. En lugar de preguntar una y otra vez, tu app puede esperar y reaccionar cuando la base de datos envía una pequeña señal.
Encaja bien en UIs donde un empujón basta. Por ejemplo:
La compensación es simplicidad frente a garantías. LISTEN/NOTIFY es fácil de añadir porque ya está en Postgres, pero no es un sistema completo de mensajería. La notificación es una pista, no un registro duradero. Si un listener está desconectado, puede perder la señal.
Una forma práctica de usarlo: deja que NOTIFY despierte tu app y luego que tu app lea la verdad desde las tablas.
Piensa en PostgreSQL LISTEN/NOTIFY como un timbre sencillo integrado en tu base de datos. Tu app puede esperar a que suene el timbre, y otra parte del sistema puede tocarlo cuando algo cambia.
Una notificación tiene dos partes: un nombre de canal y un payload opcional. El canal es como una etiqueta de tema (por ejemplo, orders_changed). El payload es un mensaje de texto corto que adjuntas (por ejemplo, un id de pedido). PostgreSQL no impone ninguna estructura, así que los equipos suelen enviar pequeñas cadenas JSON.
Una notificación puede ser desencadenada desde el código de la aplicación (tu servidor API ejecuta NOTIFY) o desde la propia base de datos usando un trigger (un trigger ejecuta NOTIFY después de un insert/update/delete).
En el lado receptor, tu servidor abre una conexión a la base de datos y ejecuta LISTEN channel_name. Esa conexión se mantiene abierta. Cuando ocurre NOTIFY channel_name, 'payload', PostgreSQL empuja un mensaje a todas las conexiones que están escuchando ese canal. Tu app entonces reacciona (refrescar cache, obtener la fila cambiada, enviar un evento WebSocket al navegador, etc.).
NOTIFY se entiende mejor como una señal, no como un servicio de entrega:
Usado así, PostgreSQL LISTEN/NOTIFY puede impulsar actualizaciones en vivo de la UI sin añadir infraestructura extra.
LISTEN/NOTIFY brilla cuando tu UI solo necesita un empujón de que algo cambió, no un stream completo de eventos. Piensa en “refresca este widget” o “hay un nuevo ítem” en lugar de “procesa cada clic en orden”.
Funciona mejor cuando la base de datos ya es tu fuente de verdad y quieres que la UI se mantenga en sincronía con ella. Un patrón común es: escribe la fila, envía una pequeña notificación que incluya un ID, y deja que la UI (o una API) obtenga el estado más reciente.
LISTEN/NOTIFY suele ser suficiente cuando la mayoría de estas condiciones se cumplen:
Un ejemplo concreto: un dashboard interno muestra “tickets abiertos” y una insignia de “notas nuevas”. Cuando un agente añade una nota, tu backend la escribe en Postgres y NOTIFY ticket_changed con el ID del ticket. El navegador la recibe vía WebSocket y vuelve a pedir esa tarjeta de ticket. Sin infraestructura extra, la UI se siente en vivo.
LISTEN/NOTIFY puede funcionar muy bien al principio, pero tiene límites claros. Esos límites aparecen cuando tratas las notificaciones como un sistema de mensajería en lugar de un simple “toque en el hombro”.
La brecha más grande es la durabilidad. Un NOTIFY no es un job en cola. Si nadie está escuchando en ese momento, el mensaje se pierde. Incluso cuando un listener está conectado, un crash, deploy, problema de red o un reinicio de la BD puede cerrar la conexión. No vas a recuperar automáticamente las notificaciones “perdidas”.
Las desconexiones son especialmente dolorosas para funciones orientadas al usuario. Imagina un dashboard que muestra nuevos pedidos. Una pestaña del navegador se suspende, el WebSocket se reconecta y la UI parece “atascada” porque perdió algunos eventos. Puedes mitigar esto, pero la solución ya no es “solo LISTEN/NOTIFY”: reconstruyes el estado consultando la base de datos y usas NOTIFY solo como pista para refrescar.
El fan-out es otro problema común. Un evento puede despertar a cientos o miles de listeners (muchas instancias de app, muchos usuarios). Si usas un canal ruidoso como orders, todos los listeners se despiertan aunque solo a un usuario le importe. Eso puede crear picos de CPU y presión de conexiones en el peor momento.
El tamaño del payload y la frecuencia son las trampas finales. Los payloads de NOTIFY son pequeños, y eventos de alta frecuencia pueden acumularse más rápido de lo que los clientes pueden manejar.
Atento a estas señales:
En ese punto, mantiene NOTIFY como un “toque” y mueve la fiabilidad a una tabla o a un broker adecuado.
Un patrón fiable con LISTEN/NOTIFY es tratar NOTIFY como un empujón, no como la fuente de verdad. La fila en la base de datos es la verdad; la notificación dice a tu app cuándo mirar.
Haz la escritura dentro de una transacción y solo envía la notificación después de que el cambio de datos esté comprometido. Si notificas demasiado pronto, los clientes pueden despertarse y no encontrar aún los datos.
Un setup común es un trigger que se dispara en INSERT/UPDATE y envía un mensaje pequeño.
NOTIFY dashboard_updates, '{\\\"type\\\":\\\"order_changed\\\",\\\"order_id\\\":123}'::text;
El nombrado de canales funciona mejor cuando coincide con cómo la gente piensa sobre el sistema. Ejemplos: dashboard_updates, user_notifications, o por tenant como tenant_42_updates.
Mantén el payload pequeño. Pon identificadores y un tipo, no registros completos. Una forma útil por defecto es:
type (qué pasó)id (qué cambió)tenant_id o user_idEsto reduce el ancho de banda y evita filtrar datos sensibles en logs de notificaciones.
Las conexiones se caen. Planifícalo.
Al conectar, ejecuta LISTEN para todos los canales necesarios. Al desconectar, reconecta con un backoff corto. Al reconectar, ejecuta LISTEN de nuevo (las suscripciones no se mantienen). Tras reconectar, haz un refetch rápido de “cambios recientes” para cubrir eventos perdidos.
Para la mayoría de actualizaciones en vivo, volver a buscar es lo más seguro: el cliente recibe {type, id} y luego pide al servidor el estado más reciente.
El parche incremental puede ser más rápido, pero es más fácil que salga mal (eventos fuera de orden, fallos parciales). Un buen punto medio: refetch de pequeñas porciones (una fila de pedido, una tarjeta de ticket, un conteo) y deja agregados más pesados en un temporizador corto.
Cuando pasas de un dashboard administrativo a muchos usuarios observando los mismos números, las buenas prácticas importan más que SQL ingenioso. LISTEN/NOTIFY puede seguir funcionando, pero necesitas modelar cómo fluyen los eventos desde la BD hasta los navegadores.
Una base común es: cada instancia de app abre una conexión de larga duración que LISTEN, y luego empuja actualizaciones a los clientes conectados. Este “un listener por instancia” es simple y suele bastar si tienes pocas instancias y toleras reconexiones ocasionales.
Si tienes muchas instancias (o workers serverless), un servicio listener compartido puede ser más fácil. Un proceso pequeño escucha una vez y hace fan-out de actualizaciones al resto del stack. También te da un lugar para añadir batching, métricas y control de presión.
Para los navegadores, normalmente empujas con WebSockets (bidireccional, ideal para UIs interactivas) o Server-Sent Events (SSE) (unidireccional, más simple para dashboards). En cualquier caso, evita enviar “refrescar todo”. Envía señales compactas como “order 123 changed” para que la UI vuelva a pedir solo lo que necesita.
Para evitar que la UI haga re-render continuo, añade algunas protecciones:
El diseño de canales también importa. En vez de un canal global, particiona por tenant, equipo o feature para que los clientes solo reciban eventos relevantes. Ejemplo: notify:tenant_42:billing y notify:tenant_42:ops.
LISTEN/NOTIFY parece simple, por eso los equipos lo lanzan rápido y luego se sorprenden en producción. La mayoría de problemas vienen de tratarlo como una cola de mensajes garantizada.
Si tu app se reconecta (deploy, fallo de red, failover), cualquier NOTIFY enviado mientras estabas desconectado se pierde. La solución es hacer de la notificación una señal y volver a chequear la BD.
Un patrón práctico: almacena el evento real en una tabla (con id y created_at), luego al reconectar busca todo lo más nuevo que tu último id visto.
Los payloads no son para grandes blobs JSON. Payloads grandes generan trabajo extra, más parsing y más posibilidades de alcanzar límites.
Usa payloads para pistas tiny como "order:123". Luego la app lee el estado más reciente de la base de datos.
Un error común es diseñar la UI alrededor del contenido del payload, como si fuera la fuente de verdad. Eso hace que cambios de esquema y versiones de cliente sean dolorosos.
Mantén una separación clara: notifica que algo cambió y luego obtén los datos actuales con una consulta normal.
Triggers que NOTIFY en cada cambio de fila pueden saturar tu sistema, especialmente en tablas con mucho tráfico.
Notifica solo en transiciones significativas (por ejemplo, cambios de estado). Si tienes updates muy ruidosos, agrupa cambios (un notify por transacción o por ventana de tiempo) o saca esas actualizaciones de la ruta de notificación.
Aunque la BD pueda mandar notificaciones, la UI aún puede colapsar. Un dashboard que re-renderiza en cada evento puede congelarse.
Debounce en el cliente, colapsa ráfagas en un solo refresco y prefiere “invalidar y volver a pedir” sobre “aplicar cada delta”. Por ejemplo: el icono de notificaciones puede actualizarse instantáneamente, pero la lista desplegable puede refrescarse como máximo una vez cada pocos segundos.
LISTEN/NOTIFY es excelente cuando quieres una pequeña señal de “algo cambió” para que la app vuelva a pedir datos frescos. No es un sistema de mensajería completo.
Antes de construir la UI alrededor, responde estas preguntas:
Una regla práctica: si puedes tratar a NOTIFY como un empujón (“ve y vuelve a leer la fila”) en lugar de como el payload en sí, estás en la zona segura.
Ejemplo: un dashboard admin muestra nuevos pedidos. Si se pierde una notificación, el siguiente polling o un refresco de página sigue mostrando el conteo correcto. Eso encaja bien. Pero si envías eventos tipo “cobrar esta tarjeta” o “enviar este paquete”, perder uno es un incidente real.
Imagina una pequeña app de ventas: un dashboard muestra los ingresos del día, total de pedidos y una lista de “órdenes recientes”. Al mismo tiempo, cada vendedor debe recibir una notificación rápida cuando una orden que le pertenece sea pagada o enviada.
Un enfoque simple es tratar a PostgreSQL como fuente de verdad y usar LISTEN/NOTIFY solo como un toque para decir que algo cambió.
Cuando se crea una orden o cambia su estado, tu backend hace dos cosas en una petición: escribe la fila (o la actualiza) y luego envía un NOTIFY con un payload tiny (a menudo solo el ID y el tipo de evento). La UI no depende del payload de NOTIFY para los datos completos.
Un flujo práctico sería:
orders_events con {\\\"type\\\":\\\"status_changed\\\",\\\"order_id\\\":123}.Esto mantiene NOTIFY ligero y limita consultas costosas.
Cuando el tráfico crece, aparecen grietas: picos de eventos pueden saturar un listener, las notificaciones se pierden al reconectar y necesitas entrega garantizada y replay. Ahí es cuando generalmente añades una capa más fiable (una tabla outbox más un worker, y luego un broker si hace falta) manteniendo Postgres como fuente de verdad.
LISTEN/NOTIFY es genial cuando necesitas una señal rápida de “algo cambió”. No está diseñado como un sistema de mensajería completo. Cuando empiezas a depender de eventos como fuente de verdad, es momento de añadir un broker.
Si aparece alguna de estas, un broker te ahorrará problemas:
LISTEN/NOTIFY no almacena mensajes para después. Es una señal push, no un log persistente. Perfecto para “refresca este widget”, arriesgado para “procesa facturación” o “envía un paquete”.
Un broker te da un modelo real de flujo de mensajes: colas (trabajo por hacer), topics (broadcast a muchos), retención (guardar mensajes minutos o días) y acknowledgments (un consumidor confirma el procesamiento). Eso te permite separar “la base de datos cambió” de “todo lo que debe pasar porque cambió”.
No hace falta elegir la herramienta más compleja. Opciones comunes: Redis (pub/sub o streams), NATS, RabbitMQ y Kafka. La elección depende de si necesitas colas de trabajo simples, fan-out a muchos servicios o la capacidad de reproducir historia.
Puedes moverte sin reescritura grande. Un patrón práctico es mantener NOTIFY como señal de despertado mientras el broker se convierte en la fuente de entrega.
Empieza escribiendo una fila de evento en una tabla dentro de la misma transacción que tu cambio de negocio, luego ten un worker que publique ese evento al broker. Durante la transición, NOTIFY puede seguir avisando a la capa de UI “revisa por nuevos eventos”, mientras los workers consumen del broker con reintentos y auditoría.
Así, los dashboards siguen rápidos y los flujos críticos dejan de depender de notificaciones de mejor esfuerzo.
Elige una pantalla (un mosaico del dashboard, un contador de badge, un toast de “nueva notificación”) y conéctala de extremo a extremo. Con LISTEN/NOTIFY puedes obtener un resultado útil rápido, siempre que mantengas el alcance limitado y midas qué ocurre bajo tráfico real.
Empieza con el patrón fiable más simple: escribe la fila, confirma y luego emite una pequeña señal de que algo cambió. En la UI, reacciona a la señal pidiendo el estado más reciente (o la porción que necesitas). Esto mantiene los payloads pequeños y evita errores sutiles cuando los mensajes llegan fuera de orden.
Añade observabilidad básica desde el inicio. No necesitas herramientas sofisticadas, pero sí respuestas cuando el sistema se vuelve ruidoso:
Mantén contratos sencillos y escritos. Decide nombres de canales, nombres de eventos y la forma de cualquier payload (aunque sea solo un ID). Un breve “catálogo de eventos” en tu repo evita deriva.
Si construyes rápido y quieres mantener la pila simple, una plataforma como Koder.ai (koder.ai) puede ayudarte a lanzar la primera versión con UI en React, backend en Go y PostgreSQL, y luego iterar según tus necesidades.
Usa LISTEN/NOTIFY cuando solo necesitas una señal rápida de que algo cambió, por ejemplo para refrescar el contador de un badge o un mosaico del dashboard. Trata la notificación como un empujón para volver a leer los datos reales en las tablas, no como la fuente de datos en sí.
El polling comprueba cambios en un horario fijo, por lo que los usuarios suelen ver actualizaciones con retraso y tu servidor hace trabajo incluso cuando no hay cambios. LISTEN/NOTIFY empuja una pequeña señal justo cuando ocurre el cambio, lo que suele sentirse más rápido y evita muchas peticiones vacías.
No, es de mejor esfuerzo. Si el listener está desconectado durante un NOTIFY, puede perder la señal porque las notificaciones no se almacenan para reproducirse más tarde.
Mantenlo pequeño y trátalo como una pista. Un valor práctico por defecto es un pequeño JSON con type e id, y luego tu app consulta Postgres por el estado actual.
Un patrón común es enviar la notificación solo después de que el write se haya confirmado (commit). Si notificas demasiado pronto, el cliente puede despertarse y no encontrar la fila nueva aún.
El código de aplicación suele ser más fácil de entender y probar porque es explícito. Los triggers son útiles cuando muchos escritores modifican la misma tabla y quieres un comportamiento consistente sin importar quién hizo el cambio.
Planifica las reconexiones como comportamiento normal. Al reconectar, vuelve a ejecutar LISTEN para los canales necesarios y haz un refetch rápido del estado reciente para cubrir lo que pudiste haber perdido mientras estabas offline.
No pongas que cada navegador se conecte a Postgres. Lo típico es una conexión larga por instancia backend que LISTEN, y luego ese backend reenvía eventos a los navegadores vía WebSockets o SSE; la UI vuelve a consultar lo que necesita.
Usa canales más específicos para que solo despierten los consumidores adecuados y agrupa las ráfagas ruidosas. Debounce de unos cientos de milisegundos y coalescer actualizaciones duplicadas evita que la UI y el backend se saturen.
Pásate cuando necesites durabilidad, reintentos, grupos de consumidores, garantías de orden o auditoría/replay. Si perder un evento causa un incidente real (facturación, envíos), usa una tabla outbox más un worker o un broker dedicado en lugar de depender solo de NOTIFY.