PostgreSQL LISTEN/NOTIFY pode alimentar dashboards e alertas em tempo real com configuração mínima. Saiba onde ele se encaixa, seus limites e quando adicionar um broker.

“Atualizações em tempo real” na interface de um produto normalmente significa que a tela muda logo depois que algo acontece, sem o usuário precisar atualizar a página. Um número aumenta no dashboard, um badge vermelho aparece na caixa de entrada, um administrador vê um pedido novo, ou aparece um toast dizendo “Build finished” ou “Payment failed”. O importante é o timing: parece instantâneo, mesmo que leve um ou dois segundos.
Muitas equipes começam com polling: o navegador pergunta ao servidor “tem algo novo?” a cada poucos segundos. Polling funciona, mas tem duas desvantagens comuns.
Primeiro, parece lento porque o usuário só vê mudanças no próximo poll.
Segundo, pode ficar caro porque você faz verificações repetidas mesmo quando nada mudou. Multiplique isso por milhares de usuários e vira ruído.
PostgreSQL LISTEN/NOTIFY existe para um caso mais simples: “me avise quando algo mudou.” Em vez de perguntar repetidamente, sua aplicação pode esperar e reagir quando o banco envia um pequeno sinal.
É uma boa opção para UIs onde um empurrão é suficiente. Por exemplo:
A troca é simplicidade versus garantias. LISTEN/NOTIFY é fácil de adicionar porque já está no Postgres, mas não é um sistema completo de mensageria. A notificação é uma dica, não um registro durável. Se um listener estiver desconectado, pode perder o sinal.
Uma forma prática de usar: deixe o NOTIFY acordar sua aplicação e, então, faça sua aplicação ler a verdade das tabelas.
Pense no PostgreSQL LISTEN/NOTIFY como uma campainha simples embutida no seu banco de dados. Sua aplicação pode esperar pela campainha tocar, e outra parte do sistema pode tocá-la quando algo mudar.
Uma notificação tem duas partes: um nome de canal e um payload opcional. O canal é como um rótulo de tópico (por exemplo, orders_changed). O payload é uma pequena mensagem de texto que você anexa (por exemplo, um id de pedido). O PostgreSQL não impõe estrutura, então equipes frequentemente enviam strings JSON pequenas.
Uma notificação pode ser disparada a partir do código da aplicação (seu servidor API executa NOTIFY) ou a partir do próprio banco usando um trigger (um trigger executa NOTIFY após um insert/update/delete).
No lado receptor, seu servidor de aplicação abre uma conexão com o banco e executa LISTEN channel_name. Essa conexão fica aberta. Quando um NOTIFY channel_name, 'payload' acontece, o PostgreSQL envia uma mensagem para todas as conexões que escutam naquele canal. Sua aplicação então reage (atualiza cache, busca a linha alterada, envia um evento via WebSocket para o navegador, e assim por diante).
NOTIFY é melhor entendido como um sinal, não um serviço de entrega:
Usado dessa forma, PostgreSQL LISTEN/NOTIFY pode alimentar atualizações de UI em tempo real sem adicionar infraestrutura extra.
LISTEN/NOTIFY brilha quando sua UI só precisa de um empurrão indicando que algo mudou, não de um fluxo completo de eventos. Pense em “recarregue esse widget” ou “há um item novo” em vez de “processe cada clique em ordem”.
Funciona melhor quando o banco de dados já é sua fonte de verdade e você quer que a UI permaneça em sincronia com ele. Um padrão comum é: escreva a linha, envie uma notificação pequena com um ID, e faça a UI (ou uma API) buscar o estado mais recente.
LISTEN/NOTIFY normalmente é suficiente quando a maioria destes pontos é verdadeira:
Um exemplo concreto: um painel interno de suporte mostra “tickets abertos” e um badge para “notas novas”. Quando um agente adiciona uma nota, seu backend grava no Postgres e NOTIFY em ticket_changed com o ID do ticket. O navegador recebe via WebSocket e busca aquele cartão de ticket. Sem infraestrutura extra, a UI parece em tempo real.
LISTEN/NOTIFY pode parecer ótimo no começo, mas tem limites rígidos. Esses limites surgem quando você trata notificações como um sistema de mensagens em vez de um leve “toque no ombro”.
A maior lacuna é durabilidade. Um NOTIFY não é um job enfileirado. Se ninguém estiver ouvindo naquele momento, a mensagem é perdida. Mesmo quando um listener está conectado, uma queda, deploy, problema de rede ou reinício do banco pode fechar a conexão. Você não recebe automaticamente as notificações “perdidas”.
Desconexões são especialmente dolorosas para funcionalidades voltadas ao usuário. Imagine um dashboard que mostra novos pedidos. Uma aba do navegador entra em modo de suspensão, o WebSocket reconecta e a UI fica “travada” porque perdeu alguns eventos. Dá para contornar, mas o workaround deixa de ser “apenas LISTEN/NOTIFY”: você reconstrói o estado consultando o banco e usando NOTIFY só como um lembrete para atualizar.
Fan-out é outro problema comum. Um evento pode acordar centenas ou milhares de listeners (muitos servidores, muitos usuários). Se você usa um canal ruidoso como orders, todo listener acorda mesmo se só um usuário se importa. Isso pode criar picos de CPU e pressão nas conexões no pior momento.
Tamanho do payload e frequência são as armadilhas finais. Payloads do NOTIFY são pequenos, e eventos em alta frequência podem se acumular mais rápido do que os clientes conseguem processar.
Fique atento a estes sinais:
Nesse ponto, mantenha o NOTIFY como um “cutucão” e leve a confiabilidade para uma tabela ou para um message broker apropriado.
Um padrão confiável com LISTEN/NOTIFY é tratar o NOTIFY como um empurrão, não como a fonte de verdade. A linha na base é a verdade; a notificação diz à aplicação quando olhar.
Faça a escrita dentro de uma transação e só envie a notificação após a mudança de dados ser comitada. Se você notificar cedo demais, clientes podem acordar e não encontrar os dados.
Uma configuração comum é um trigger que dispara no INSERT/UPDATE e envia uma pequena mensagem.
NOTIFY dashboard_updates, '{"type":"order_changed","order_id":123}'::text;
Nomear canais funciona melhor quando corresponde a como as pessoas pensam sobre o sistema. Exemplos: dashboard_updates, user_notifications ou por tenant, como tenant_42_updates.
Mantenha o payload pequeno. Coloque identificadores e um tipo, não registros completos. Uma forma útil padrão é:
type (o que aconteceu)id (o que mudou)tenant_id ou user_idIsso mantém a largura de banda baixa e evita vazar dados sensíveis nos logs de notificação.
Conexões caem. Planeje isso.
Ao conectar, execute LISTEN para todos os canais necessários. Ao desconectar, reconecte com um short backoff. Ao reconectar, execute LISTEN novamente (as subscrições não persistem). Depois da reconexão, faça um refetch rápido de “mudanças recentes” para cobrir eventos perdidos.
Para a maioria das atualizações em tempo real, refazer a busca é a opção mais segura: o cliente recebe {type, id} e então pede ao servidor o estado mais recente.
Patch incremental pode ser mais rápido, mas é mais fácil errar (eventos fora de ordem, falhas parciais). Um bom meio-termo é: refetch de fatias pequenas (uma linha de pedido, um cartão de ticket, a contagem de um badge) e deixar agregados pesados em um timer curto.
Quando você passa de um dashboard de admin para muitos usuários assistindo os mesmos números, bons hábitos importam mais do que SQL esperto. LISTEN/NOTIFY ainda pode funcionar bem, mas você precisa modelar como eventos fluem do banco para os browsers.
Uma base comum é: cada instância de app abre uma conexão de longa duração que faz LISTEN, e então empurra atualizações para os clientes conectados. Esse “um listener por instância” é simples e costuma dar conta se você tem poucas instâncias e tolera reconexões ocasionais.
Se você tem muitas instâncias (ou workers serverless), um serviço de listener compartilhado pode ser mais fácil. Um processo pequeno escuta uma vez e faz fan-out para o restante da stack. Também é um lugar único para adicionar batching, métricas e backpressure.
Para navegadores, normalmente você envia via WebSockets (bidirecional, ótimo para UIs interativas) ou Server-Sent Events (SSE) (unidirecional, mais simples para dashboards). De qualquer forma, evite enviar “recarregue tudo.” Envie sinais compactos como “order 123 mudou” para que a UI refaça apenas o que precisa.
Para evitar thrashing na UI, adicione guardrails:
O design dos canais também importa. Em vez de um canal global, particione por tenant, time ou feature para que clientes só recebam eventos relevantes. Exemplo: notify:tenant_42:billing e notify:tenant_42:ops.
LISTEN/NOTIFY parece simples, por isso equipes entregam rápido e depois se surpreendem em produção. A maioria dos problemas vem de tratá-lo como uma fila de mensagens durável.
Se sua aplicação reconectar (deploy, queda de rede, failover), qualquer NOTIFY enviado enquanto você estava desconectado se perde. A correção é tratar a notificação como um sinal e então re-checar o banco.
Um padrão prático: armazene o evento real em uma tabela (com id e created_at) e, ao reconectar, busque tudo mais novo que seu último id visto.
Payloads do LISTEN/NOTIFY não são para JSONs grandes. Payloads grandes criam trabalho extra, parse adicional e mais chance de atingir limites.
Use payloads como dicas pequenas, tipo "order:123". Depois a aplicação lê o estado mais recente no banco.
Um erro comum é desenhar a UI em torno do conteúdo do payload, como se ele fosse a fonte de verdade. Isso dificulta mudanças de esquema e versões de cliente.
Mantenha uma divisão clara: notifique que algo mudou e depois busque os dados atuais com uma query normal.
Triggers que NOTIFY em toda mudança de linha podem inundar seu sistema, especialmente em tabelas muito ativas.
Notifique só em transições significativas (por exemplo, mudança de status). Se tiver updates muito ruidosos, agrupe mudanças (um notify por transação ou por janela de tempo) ou tire essas atualizações do caminho de notify.
Mesmo que o banco consiga enviar notificações, sua UI pode travar. Um dashboard que re-renderiza em todo evento pode congelar.
Debounce no cliente, colapse rajadas em um único refresh e prefira “invalidar e refazer” a “aplicar todo delta”. Por exemplo: o ícone de notificação pode atualizar instantaneamente, mas a lista detalhada pode atualizar no máximo a cada poucos segundos.
LISTEN/NOTIFY é ótimo quando você quer um pequeno sinal “algo mudou” para que a aplicação busque dados frescos. Não é um sistema de mensageria completo.
Antes de construir a UI em torno dele, responda:
Uma regra prática: se você puder tratar NOTIFY como um empurrão (“vá reler a linha”) em vez de como o payload em si, você está na zona segura.
Exemplo: um dashboard de admin mostra novos pedidos. Se uma notificação for perdida, o próximo poll ou refresh ainda mostra a contagem correta. Isso é um bom uso. Mas se você envia eventos como “cobrar este cartão” ou “enviar este pacote”, perder um evento é um incidente grave.
Imagine um pequeno app de vendas: um dashboard mostra a receita do dia, total de pedidos e uma lista de “pedidos recentes”. Ao mesmo tempo, cada vendedor deve receber uma notificação rápida quando um pedido que ele gerencia for pago ou enviado.
Uma abordagem simples é tratar o PostgreSQL como fonte de verdade e usar LISTEN/NOTIFY apenas como um toque no ombro indicando que algo mudou.
Quando um pedido é criado ou seu status muda, seu backend faz duas coisas na mesma requisição: grava (ou atualiza) a linha e então envia um NOTIFY com um payload pequeno (geralmente apenas o ID do pedido e o tipo de evento). A UI não depende do payload do NOTIFY para os dados completos.
Um fluxo prático fica assim:
orders_events com {"type":"status_changed","order_id":123}.Isso mantém o NOTIFY leve e limita queries caras.
Quando o tráfego cresce, aparecem falhas: um pico de eventos pode sobrecarregar um listener, notificações podem ser perdidas na reconexão e você passa a precisar de entrega garantida e replay. É aí que normalmente se adiciona uma camada mais confiável (tabela outbox + worker, depois um broker se necessário), mantendo o Postgres como fonte de verdade.
LISTEN/NOTIFY é ótimo para um sinal rápido “algo mudou”. Não foi construído para ser um sistema de mensageria completo. Quando você começa a depender de eventos como fonte de verdade, está na hora de um broker.
Se algum destes ocorrer, um broker vai poupar dores:
LISTEN/NOTIFY não armazena mensagens para depois. É um sinal push, não um log persistido. Perfeito para “recarregue este widget do dashboard”, arriscado para “dispare cobrança” ou “envie pacotes”.
Um broker oferece um modelo real de fluxo de mensagens: filas (trabalho a ser feito), tópicos (broadcast para muitos), retenção (guardar mensagens por minutos ou dias) e acknowledgments (o consumidor confirma o processamento). Isso separa “o banco mudou” de “tudo que deve acontecer por causa disso”.
Você não precisa escolher a ferramenta mais complexa. Opções comuns são Redis (pub/sub ou streams), NATS, RabbitMQ e Kafka. A escolha depende se você precisa de filas simples, fan-out para muitos serviços ou habilidade de replay de histórico.
É possível migrar sem grande refatoração. Um padrão prático é manter NOTIFY como sinal enquanto o broker vira a fonte de entrega.
Comece escrevendo uma “linha de evento” em uma tabela dentro da mesma transação da mudança de negócio e então tenha um worker que publica esse evento no broker. Durante a transição, NOTIFY pode continuar informando a camada de UI para “verificar novos eventos”, enquanto workers de background consomem do broker com retries e auditoria.
Assim dashboards continuam rápidos e workflows críticos param de depender de notificações best-effort.
Escolha uma tela (um tile do dashboard, a contagem de um badge, um toast de “nova notificação”) e conecte tudo ponta a ponta. Com LISTEN/NOTIFY você consegue um resultado útil rapidamente, desde que mantenha o escopo limitado e meça o que acontece com tráfego real.
Comece com o padrão mais simples e confiável: escreva a linha, comite e então emita um pequeno sinal de que algo mudou. Na UI, reaja ao sinal buscando o estado mais recente (ou a fatia necessária). Isso mantém payloads pequenos e evita bugs sutis quando mensagens chegam fora de ordem.
Adicione observabilidade básica cedo. Não precisa de ferramentas sofisticadas para começar, mas precisa de respostas quando o sistema ficar barulhento:
Mantenha contratos simples e documentados. Decida nomes de canais, nomes de eventos e o formato do payload (mesmo que seja só um ID). Um curto “catálogo de eventos” no repositório evita divergências.
Se você está construindo rápido e quer manter a stack simples, uma plataforma como Koder.ai (koder.ai) pode ajudar a lançar a primeira versão com UI em React, backend em Go e PostgreSQL, e então iterar conforme suas necessidades ficarem mais claras.
Use LISTEN/NOTIFY quando você precisa apenas de um sinal rápido de que algo mudou, como atualizar a contagem de um badge ou um bloco do dashboard. Trate a notificação como um empurrão para refazer a leitura dos dados nas tabelas, não como a fonte de verdade em si.
O polling verifica mudanças em intervalos definidos, então os usuários geralmente veem atualizações com atraso e seu servidor faz trabalho mesmo quando nada mudou. LISTEN/NOTIFY empurra um pequeno sinal no momento da mudança, o que costuma parecer mais rápido e evita muitas requisições vazias.
Não, é best-effort. Se o listener estiver desconectado durante um NOTIFY, ele pode perder o sinal porque as notificações não são armazenadas para reprodução posterior.
Mantenha pequeno e trate como uma dica. Um padrão prático é uma pequena string JSON com type e id, e então sua aplicação consulta o Postgres para o estado atual.
Um padrão comum é enviar a notificação só depois que a escrita for comitada. Se você notificar cedo demais, o cliente pode acordar e não encontrar a nova linha ainda.
Código de aplicação costuma ser mais fácil de entender e testar, porque é explícito. Triggers são úteis quando múltiplos escritores tocam a mesma tabela e você quer comportamento consistente independentemente de quem fez a mudança.
Planeje reconexões como comportamento normal. Ao reconectar, execute LISTEN para os canais necessários e faça um refetch rápido do estado recente para cobrir o que você pode ter perdido enquanto esteve offline.
Não faça com que cada navegador conecte ao Postgres. Uma configuração típica é uma conexão de listener por instância de backend, que então encaminha eventos para os navegadores via WebSockets ou SSE; a UI refaz o que precisa ao receber o sinal.
Use canais mais estreitos para que só os consumidores relevantes acordem e agrupe rajadas ruidosas. Debounce por algumas centenas de milissegundos e coalesce atualizações duplicadas para evitar que UI e backend travem.
Pare de usar quando você precisar de durabilidade, retries, grupos de consumidores, garantias de ordenação ou auditoria/replay. Se perder um evento causar um incidente real (cobrança, envio, workflows irreversíveis), use uma tabela outbox + worker ou um broker dedicado em vez de confiar só no NOTIFY.