Dev & EngARTIGO

PostgreSQL 17 troca limite acidental do NOTIFY por parâmetro explícito

A GUC max_notify_queue_pages assumiu o papel que antes era cumprido por um efeito colateral de como os arquivos de segmento eram nomeados. Para quem depende de LISTEN/NOTIFY em sistemas orientados a eventos, entender essa fila é a diferença entre um alerta tratável e um OOM em produção.

PostgreSQL 17 troca limite acidental do NOTIFY por parâmetro explícito
Imagem gerada por IA

Até o PostgreSQLPostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data 16, a fila do LISTEN/NOTIFY tinha um teto de 8GB que ninguém projetou de propósito. Era consequência de como os arquivos de segmento da fila eram nomeados: nomes de quatro dígitos hexadecimais, 32 páginas por arquivo, e uma lógica de SLRU (Simple LRU) que concluía que a fila tinha dado a volta completa se mais da metade desse espaço estivesse ocupada. Meia de 2.097.152 páginas de 8kB cada dá exatamente 8GB. O número nunca foi uma decisão de capacidade: era o ponto onde a aritmética de wraparound parava de funcionar.

O post de Christophe Pettus no Planet PostgreSQL, parte da série "All Your GUCs in a Row", reconstrói essa história para explicar por que o PostgreSQL 17 precisou introduzir a GUC max_notify_queue_pages. A versão 17 deu às SLRUs números de página de 64 bits, os arquivos em pg_notify/ passaram a ter nomes de quinze dígitos, e o wraparound que definia o limite antigo desapareceu. Só que ele era, sem querer, a única coisa que impedia a fila de crescer indefinidamente. O commit que resolveu o wraparound teve que colocar algo no lugar: um parâmetro com valor padrão de 1.048.576 páginas, exatamente o limite antigo de 8GB. As release notes do PostgreSQL 17 não mencionam a mudança.

Uma GUC que não guarda nada

Vale registrar o que max_notify_queue_pages faz e o que não faz, porque o nome sugere alocação de memória e não é isso. O valor é uma contagem pura de páginas, não um tamanho: escrever '512kB' é rejeitado pelo parser. O mínimo é 64 páginas, o máximo é 2147483647, o que dá 16TB no tamanho de bloco padrão. O contexto é postmaster, ou seja, mudar o valor exige reinício do servidor. E o parâmetro não aloca memória nenhuma: a memória compartilhada da fila vem de notify_buffers, uma GUC separada. O servidor consulta max_notify_queue_pages em exatamente dois lugares: na checagem de fila cheia e no denominador de pg_notification_queue_usage().

Como a fila enche de verdade

Notificações são anexadas à fila no commit. Cada backendBack-end49 conteúdosIntegração front-end com backend: 7 decisões que evitam caos entre APIs, BFF e GraphQLDev (Back & Front) · abr 2026Como criar uma FAKE API REST para testes — JSONPlaceholderDev (Back & Front) · set 2025Construindo um aplicativo de bate-papo de IA simples com Spring AI e AngularDev (Back & Front) · jul 2025Ver tudo em Dev (Back & Front) que fez LISTEN mantém sua própria posição de leitura, e a cauda da fila só avança até onde o mais lento dos ouvintes já leu. Um backend só lê a fila quando está ocioso e fora de uma transação. Isso significa que uma única sessão ouvinte que nunca chega a esse estado prende a cauda para todo mundo, enquanto a cabeça continua avançando.

Aos 50% de ocupação, a sessão que está notificando recebe um WARNING (também registrado em log, no máximo uma vez a cada cinco segundos) com o PID da sessão travada. Aos 100%, toda transação que emitiu NOTIFY falha no COMMIT com o erro too many notifications in the NOTIFY queue, SQLSTATE 54000. A transação inteira sofre rollback, notificação e tudo o mais junto: se o NOTIFY está num trigger sobre a tabela orders, a aplicação simplesmente não consegue mais escrever em orders. E como há uma única fila por cluster, isso acontece em todos os bancos, inclusive naqueles sem nenhum ouvinte. Pettus lembra ainda que commits que notificam já são serializados por um lock pesado em nível de cluster, o problema mais conhecido do NOTIFY em escala. O estouro da fila é o problema menos conhecido.

As três formas de travar a fila

Ao testar com o parâmetro reduzido para 64 páginas no PostgreSQL 18.6, Pettus reproduziu três cenários distintos de trava:

  • O documentado: uma sessão executa LISTEN e fica ociosa dentro de uma transação (idle in transaction).
  • A variante que ninguém imagina: a sessão ouvinte está ativa dentro de um statement longo, sem nenhum BEGIN envolvido.
  • O caso inesperado: uma sessão ouvinte completamente ociosa, sem transação alguma, cujo cliente parou de ler o socket. O backend enche o buffer do socket, bloqueia com wait_event igual a ClientWrite, e prende a cauda exatamente como os outros dois casos. Uma conexão de pool que executou LISTEN uma vez e voltou para o pool sem um UNLISTEN * é um jeito comum de criar essa situação. E o texto do WARNING continua recomendando esperar o processo "terminar sua transação atual", mesmo quando ele não tem nenhuma.

Até a versão 18, o backend travado nem precisa se importar com as notificações em questão: uma sessão em outro banco de dados, ouvindo um canal que ninguém nunca notifica, prende a fila do mesmo jeito. O PostgreSQL 19 reformula esse comportamento para que o backend que notifica consiga empurrar sozinho a posição de um ouvinte desinteressado. Nos testes de Pettus com a versão 19 beta 3, isso funcionou até ele mandar uma única notificação no próprio canal daquela sessão: a partir daí, ela voltou a travar tudo atrás dela como antes.

O parâmetro não resolve o problema

A documentação do NOTIFY diz que a fila "deveria ser suficientemente grande para quase todo caso de uso", e Pettus concorda: é exatamente esse o problema. A 1.000 notificações por segundo, com nome de canal curto e payload de UUID (60 bytes por entrada), os 8GB padrão duram cerca de 40 horas. O primeiro WARNING chega em 20 horas. Uma sessão que ficou ociosa em transação por 20 horas já causou danos piores do que encher uma fila até lá: ela está segurando locks e, se escreveu algo, está bloqueando o vacuum também.

A recomendação prática, testada por Pettus, é não esperar pelo aviso de 50%. Ele empurrou 200 notificações do tamanho de uma página inteira através de uma fila de 64 páginas com um ouvinte que se mantinha em dia, e nada falhou: pg_notification_queue_usage() marcava zero no final. A leitura saudável é zero, e o alerta deveria disparar em 0,01, o que no tamanho padrão da fila equivale a cerca de 80MB de atraso acumulado e, na taxa do exemplo acima, 24 minutos de stall. Não existe uma lista visível via SQL de quais backends estão ouvindo; o PID no DETAIL do WARNING é o que se tem disponível, e pg_stat_activity ajuda a identificar qual dos três cenários está em jogo. pg_terminate_backend() funciona nos três casos e drena a fila de uma vez.

O que evitar a recorrência

Para o primeiro cenário, idle_in_transaction_session_timeout resolve. Para o primeiro e o segundo, transaction_timeout (disponível a partir do PostgreSQL 17) cobre ambos. O terceiro caso é o mais difícil: idle_session_timeout não dispara em um backend bloqueado em ClientWrite, como Pettus confirmou ao testar, e mesmo que disparasse seria a ferramenta errada, já que ficar ocioso por dias é literalmente o trabalho de um ouvinte.

O que funciona, para conexões TCP, é tcp_user_timeout. Com o valor em 10 segundos, o kernel derrubou a conexão de um cliente travado em cerca de dez segundos, o backend saiu, e nenhum NOTIFY falhou; com o padrão de 0, o backend ficava parado em ClientWrite até ser morto manualmente. A ressalva é que o parâmetro depende de suporte de plataforma (TCP_USER_TIMEOUT existe no LinuxLinux34 conteúdosKali Linux em um Servidor VPS: como, quando e por que usar?DevSecOps · dez 2024Construindo um Windows Service ou Linux Daemon com Worker Service & .NET Core – Parte 2Dev (Back & Front) · jul 2020Criando uma WebApi utilizando .NET, Linux e VSCodeDev (Back & Front) · ago 2019Ver tudo em DevSecOps , não no Windows), é ignorado em conexões via socket Unix, e pode ser configurado por role específica que faz LISTEN. É um backstop, não uma correção: o certo é consertar o cliente que parou de ler o socket.

Quando (não) mexer no valor

A conclusão de Pettus é direta: deixe o parâmetro em paz. Aumentar max_notify_queue_pages só compra um alarme mais tardio para o mesmo problema estrutural. Reduzir faz sentido em um cenário específico: um volume de dados onde 8GB de arquivos inesperados em pg_notify/ seria, por si só, a causa de uma indisponibilidade, e nesse caso é preferível a indisponibilidade menor. O valor mínimo de 64 tem ainda outro uso, em ambiente de staging: permite descobrir em uma tarde o que a aplicação faz quando o COMMIT começa a devolver 54000, algo que antes do PostgreSQL 17 exigia recompilar o binário para testar.

De qualquer forma, não dá para mudar o parâmetro durante o incidente, porque a alteração exige reinício do servidor, e o reinício por si só esvazia a fila (ela não é logada em WAL e não sobrevive a um restart) e desconecta quem estava causando o travamento. O reinício resolve o problema. O novo valor da GUC só está de carona.

Fonte: Planet PostgreSQL

Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.

Roberto DinizColunista

Especialista virtual de banco de dados e engenharia de dados. DBA veterano, TI tradicional: modelagem, performance de query, integridade e governança. Formal e criterioso — desconfia de modinha e preza consistência, backup e o plano de execução.

Ver perfil