O PostgreSQL LISTEN/NOTIFY no Node.js oferece um mecanismo simples de sinalização entre processos conectados ao mesmo banco. Uma sessão executa NOTIFY em um canal e as sessões que fizeram LISTEN recebem um evento assíncrono após o commit da transação.
Esse recurso é útil para invalidar caches, avisar workers sobre novos registros, atualizar dashboards e reduzir polling. Porém, LISTEN/NOTIFY não é uma fila durável: notificações não são armazenadas para clientes desconectados, o payload é pequeno e não existe ACK, retry ou replay.
Neste guia, você aprenderá a criar uma conexão dedicada com pg, assinar canais, publicar com pg_notify, reconectar, usar tabelas como fonte de verdade e decidir quando migrar para Kafka, RabbitMQ, Redis Streams ou SQS.
Como funciona?
A documentação oficial do PostgreSQL NOTIFY explica que o comando envia uma notificação com payload opcional para todas as sessões que executaram LISTEN no mesmo canal e banco.
O evento recebido contém:
- nome do canal;
- PID da sessão que notificou;
- payload textual;
- ordem de commit das transações.
Instalação
npm install pgCrie uma conexão dedicada:
import pg from 'pg';
const { Client } = pg;
const listener = new Client({
connectionString: process.env.DATABASE_URL,
application_name: 'orders-listener'
});
await listener.connect();
await listener.query('LISTEN order_events');Não use uma conexão temporária do pool para LISTEN. O estado de assinatura pertence à sessão. Se a conexão volta ao pool, outro código pode reutilizá-la ou encerrá-la.
Recebendo notificações
listener.on('notification', notification => {
if (notification.channel !== 'order_events') return;
try {
const event = JSON.parse(notification.payload ?? '{}');
handleNotification(event);
} catch (error) {
logger.error({ error }, 'Payload NOTIFY inválido');
}
});O handler deve ser rápido. Coloque trabalho pesado em uma fila interna ou busque dados no banco.
Enviando uma notificação
await database.query(
'SELECT pg_notify($1, $2)',
[
'order_events',
JSON.stringify({
type: 'order.updated',
orderId: 'order-42'
})
]
);pg_notify é mais fácil de parametrizar que montar um comando NOTIFY com texto dinâmico.
Entrega após commit
Quando NOTIFY é executado dentro de uma transação, a notificação só é entregue se o commit concluir:
await client.query('BEGIN');
try {
await client.query(
'UPDATE orders SET status = $1 WHERE id = $2',
['paid', 'order-42']
);
await client.query(
'SELECT pg_notify($1, $2)',
['order_events', JSON.stringify({ orderId: 'order-42' })]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
}Se ocorrer rollback, a notificação não é enviada. Isso evita avisar sobre uma mudança que não persistiu.
Transações curtas no listener
Uma sessão que está dentro de uma transação longa não entrega notificações ao cliente até terminar. Use a conexão dedicada somente para LISTEN e consultas rápidas de controle.
Payload pequeno
O payload possui limite e deve transportar apenas identificação:
{
"eventId": "evt-42",
"resourceId": "order-42",
"type": "order.updated"
}Armazene dados maiores em tabela e envie a chave. Isso também garante uma fonte de verdade durável.
Tabela de eventos
CREATE TABLE application_events (
id uuid PRIMARY KEY,
type text NOT NULL,
aggregate_id text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
processed_at timestamptz
);O produtor insere o evento e envia apenas o ID:
SELECT pg_notify('application_events', 'evt-42');O listener lê a tabela. Se perder a notificação, um processo periódico ainda encontra registros pendentes.
Trigger automático
CREATE OR REPLACE FUNCTION notify_order_change()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'order_events',
json_build_object(
'operation', TG_OP,
'orderId', NEW.id
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER orders_notify
AFTER INSERT OR UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION notify_order_change();Triggers evitam que uma aplicação esqueça de notificar, mas aumentam comportamento implícito. Documente e teste migrations.
Notificações duplicadas
Dentro da mesma transação, notificações idênticas no mesmo canal podem ser agrupadas. Payloads diferentes são entregues separadamente. Não use NOTIFY como contador exato de alterações.
Sem entrega offline
Se o cliente estiver desconectado, ele não recebe notificações antigas. Portanto:
- mantenha uma tabela durável;
- faça sincronização inicial após conectar;
- execute polling de segurança;
- trate a notificação como “há trabalho”, não como o próprio trabalho.
Reconexão
async function connectListener() {
const client = new Client({
connectionString: process.env.DATABASE_URL
});
client.on('error', error => {
logger.error({ error }, 'Listener PostgreSQL perdeu conexão');
});
await client.connect();
await client.query('LISTEN order_events');
return client;
}Após reconectar, execute LISTEN novamente e faça uma varredura na tabela, porque eventos podem ter ocorrido durante a queda.
Loop de reconexão
async function runListener() {
let attempt = 0;
while (!stopping) {
try {
const client = await connectListener();
attempt = 0;
await waitUntilClosed(client);
} catch (error) {
const base = Math.min(30_000, 500 * 2 ** attempt++);
const delay = Math.floor(base * (0.5 + Math.random()));
await sleep(delay);
}
}
}Veja Retry com Backoff no Node.js.
Múltiplas réplicas
Todas as sessões inscritas recebem a mesma notificação. Isso é broadcast, não distribuição de trabalho. Se cinco réplicas ouvirem, as cinco podem executar o handler.
Para apenas uma processar:
- use uma fila com consumer groups;
- busque jobs com
FOR UPDATE SKIP LOCKED; - adquira advisory lock;
- torne o processamento idempotente.
Consulte Advisory Locks no Node.js.
Invalidação de cache
listener.on('notification', async notification => {
const { productId } = JSON.parse(notification.payload);
await redis.del(`product:${productId}`);
});A invalidação precisa tolerar duplicatas e perdas. Defina TTL no cache como rede de segurança.
Veja Cache Stampede no Node.js.
Atualizações em tempo real
Um servidor WebSocket pode ouvir mudanças e retransmitir aos clientes:
listener.on('notification', notification => {
const event = JSON.parse(notification.payload);
websocketHub.broadcast(event);
});Se existem várias réplicas do WebSocket, todas recebem o NOTIFY e atualizam seus próprios clientes.
Outbox e NOTIFY
NOTIFY pode acordar um relay de outbox:
- transação grava negócio e outbox;
- trigger envia NOTIFY após commit;
- relay busca registros pendentes;
- relay publica no broker;
- polling periódico cobre notificações perdidas.
Veja Outbox Pattern no Node.js.
Segurança de canais
Notificações são visíveis a sessões que conseguem LISTEN no banco. Não envie:
- tokens;
- senhas;
- dados pessoais completos;
- conteúdo de documentos;
- segredos de negócio.
Envie IDs opacos e aplique controle de acesso no banco.
Sanitizando nomes de canal
Parâmetros SQL não substituem identificadores em LISTEN canal. Use allowlist:
const allowedChannels = new Set([
'order_events',
'product_events'
]);
if (!allowedChannels.has(channel)) {
throw new Error('Canal inválido');
}Não concatene entrada do usuário diretamente.
Fila interna
O evento notification não aplica backpressure. Enfileire localmente e limite concorrência. Se o volume ultrapassa a capacidade, busque apenas IDs pendentes na tabela.
Fila de notificações do PostgreSQL
O servidor mantém uma fila interna até que listeners processem. Transações longas em listeners podem impedir limpeza. Monitore:
SELECT pg_notification_queue_usage();Se o uso cresce, investigue sessões LISTEN presas em transações.
Shutdown gracioso
async function shutdown() {
stopping = true;
await listener.query('UNLISTEN *');
await listener.end();
}
process.once('SIGTERM', shutdown);
process.once('SIGINT', shutdown);Veja Graceful Shutdown no Node.js.
Observabilidade
Meça:
- estado da conexão dedicada;
- reconexões;
- notificações por canal;
- erros de parse;
- tempo entre commit e processamento;
- registros pendentes na tabela;
- uso da fila de notificações;
- duração de transações do listener.
Testes
Use PostgreSQL real em container e teste:
- notificação após commit;
- ausência após rollback;
- payload inválido;
- desconexão e reconexão;
- eventos durante downtime;
- múltiplos listeners;
- trigger;
- shutdown.
Consulte Testcontainers no Node.js.
Quando usar
- Sinalização dentro de um sistema pequeno.
- Invalidação de cache.
- Atualização de dashboards.
- Acordar um worker que consulta tabela.
- Notificação best-effort entre processos.
Quando usar um broker
- entrega durável;
- replay;
- ACK e retry;
- consumer groups;
- alto volume;
- integração entre bancos ou regiões;
- retenção e auditoria.
Veja Kafka com Node.js e RabbitMQ com Node.js.
Erros comuns
- Conexão do pool: assinatura se perde.
- Payload como fonte de verdade: evento perdido não pode ser recuperado.
- Trabalho pesado no callback: backlog cresce.
- Assumir fila: todas as réplicas recebem.
- Sem reconciliação: downtime perde mudanças.
- Transação longa no listener: entrega atrasa.
- Dados sensíveis no payload: exposição.
- Nome de canal dinâmico inseguro: SQL injection.
Conclusão
O PostgreSQL LISTEN/NOTIFY no Node.js é uma ferramenta leve para sinalizar mudanças após commit. Uma conexão dedicada recebe eventos rapidamente sem polling contínuo.
Use payloads pequenos, tabelas como fonte de verdade, reconexão e varredura de segurança. Para entrega durável, distribuição entre consumidores e replay, escolha um broker. LISTEN/NOTIFY funciona melhor como campainha: ele avisa que algo mudou, e a aplicação consulta o estado confiável no banco.




