Abrir uma nova conexão com PostgreSQL para cada consulta aumenta latência e pode esgotar rapidamente o limite do banco. Um Pool PostgreSQL no Node.js mantém um conjunto controlado de conexões reutilizáveis, permitindo atender várias requisições sem criar um socket e autenticar novamente a cada operação.
O pool não cria capacidade infinita. Quando todas as conexões estão ocupadas, novas consultas esperam em uma fila. Se o tamanho for alto demais, várias instâncias podem ultrapassar o limite do PostgreSQL; se for baixo demais, a aplicação acumula espera. Também é essencial liberar clientes, definir timeouts e fechar o pool durante o desligamento.
Neste guia, você aprenderá a configurar pg.Pool, executar consultas simples, controlar transações, evitar vazamentos, escolher o tamanho, monitorar a fila e integrar health checks e graceful shutdown.
Instalando o node-postgres
npm install pgCrie um módulo central:
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000
});
module.exports = pool;A documentação oficial de pooling do node-postgres explica o comportamento do cliente. A documentação do PostgreSQL sobre configuração de conexões apresenta limites do servidor.
Para estruturar uma API, veja como criar uma API com Node.js e o que é Node.js.
Consulta simples com pool.query()
async function findUserById(id) {
const result = await pool.query(
'SELECT id, name, email FROM users WHERE id = $1',
[id]
);
return result.rows[0] || null;
}pool.query() pega uma conexão, executa a consulta e libera automaticamente. Essa é a opção preferida para uma única instrução que não precisa compartilhar sessão com outras.
Use parâmetros
Nunca concatene valores do usuário na SQL:
const result = await pool.query(
'SELECT * FROM products WHERE category_id = $1 LIMIT $2',
[categoryId, limit]
);Parâmetros reduzem risco de SQL injection e evitam problemas de escape. Nomes de tabela e coluna não podem ser parametrizados da mesma maneira; use uma lista permitida quando forem dinâmicos.
Obtendo um cliente dedicado
const client = await pool.connect();
try {
const result = await client.query('SELECT NOW()');
console.log(result.rows[0]);
} finally {
client.release();
}Sempre libere no bloco finally. Um caminho de erro que esquece release() mantém a conexão ocupada e, depois de algumas requisições, toda a fila trava.
Transações
Uma transação precisa usar o mesmo cliente do início ao fim:
async function transferBalance(fromId, toId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, fromId]
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toId]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}Não use pool.query() separadamente dentro da transação, pois cada chamada pode pegar uma conexão diferente.
Helper de transação
async function withTransaction(callback) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
error.rollbackError = rollbackError;
}
throw error;
} finally {
client.release();
}
}O helper centraliza rollback e liberação. Evite transações longas que fazem chamadas HTTP ou aguardam interação do usuário, pois elas mantêm conexão e locks.
Escolhendo o tamanho do pool
O valor ideal depende de:
- limite
max_connectionsdo PostgreSQL; - conexões reservadas para administração e outros serviços;
- quantidade máxima de instâncias da aplicação;
- concorrência e duração das consultas;
- uso de um pooler externo, como PgBouncer.
Se o banco aceita 100 conexões e existem 10 instâncias com pool máximo 20, o limite teórico seria 200. O dimensionamento precisa considerar o pico de escalonamento, não apenas o estado atual.
Pool grande não significa mais desempenho
PostgreSQL executando centenas de consultas simultâneas pode gastar mais tempo alternando CPU, memória e I/O. Um pool menor cria fila na aplicação e protege o banco. Meça latência total e utilização antes de aumentar.
Fila de espera
O node-postgres expõe contadores úteis:
logger.info({
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount
}, 'Estado do pool PostgreSQL');waitingCount crescente indica consultas lentas, pool pequeno, vazamento ou banco degradado. Monitore também duração das consultas.
Timeout de conexão
connectionTimeoutMillis limita o tempo para obter uma nova conexão física. Ele não necessariamente limita o tempo esperando um cliente disponível em todas as situações. Defina também um orçamento total na camada de serviço.
Timeout de consulta
Configure statement_timeout no banco, no usuário ou por sessão:
const pool = new Pool({
connectionString,
options: '-c statement_timeout=5000'
});Outra opção é executar SET statement_timeout ao conectar. O timeout precisa ser menor que o prazo da requisição e maior que o tempo normal da consulta.
Cancelamento
Quando o cliente HTTP desconecta, a aplicação pode não precisar mais do resultado. Nem todas as versões e APIs oferecem o mesmo suporte a AbortSignal, então confirme a documentação da biblioteca. Mesmo sem cancelamento do PostgreSQL, pare etapas posteriores e limite a consulta no servidor.
Veja AbortController no Node.js para propagar cancelamento.
Eventos do pool
pool.on('connect', client => {
logger.debug('Nova conexão PostgreSQL');
});
pool.on('error', error => {
logger.error({ error }, 'Erro em cliente ocioso do pool');
});
pool.on('remove', () => {
logger.debug('Conexão removida do pool');
});O evento error precisa ser tratado. Uma conexão ociosa pode falhar por reinício do banco ou problema de rede.
SSL
const pool = new Pool({
connectionString,
ssl: {
rejectUnauthorized: true,
ca: process.env.DATABASE_CA
}
});Não desative validação de certificado em produção. O provedor do banco deve fornecer a CA ou uma configuração segura. Veja HTTPS e TLS no Node.js.
Variáveis de ambiente
Valide DATABASE_URL, tamanho do pool e timeouts na inicialização. Não registre a URL completa, pois ela pode conter usuário e senha. Consulte Variáveis de Ambiente no Node.js.
Health check
async function checkDatabase() {
const startedAt = performance.now();
await pool.query('SELECT 1');
return {
status: 'up',
durationMs: performance.now() - startedAt
};
}Use a verificação em readiness, não em liveness. A consulta deve ser curta e ter timeout. Veja Health Checks no Node.js.
Graceful shutdown
async function shutdown() {
ready = false;
await closeServer(server);
await pool.end();
}Pare novas requisições antes de fechar o pool. pool.end() encerra conexões e impede novas consultas. O guia de Graceful Shutdown no Node.js detalha a ordem.
PgBouncer e pool duplo
Um pooler externo pode concentrar conexões de muitas instâncias. Nesse caso, o pool da aplicação ainda deve ter limite, mas talvez seja menor. Em modo transaction pooling, recursos dependentes de sessão, prepared statements nomeados e algumas configurações podem ter restrições.
Prepared statements
await pool.query({
name: 'find-user-by-id',
text: 'SELECT id, name FROM users WHERE id = $1',
values: [id]
});O nome permite preparar por conexão. Use em consultas realmente frequentes e estáveis; não crie nomes dinâmicos ilimitados.
N+1 e pool
Aumentar o pool não corrige uma rota que faz centenas de consultas pequenas. Reduza round trips com JOIN, agregação ou carregamento em lote. O artigo sobre performance de APIs Node.js apresenta otimizações gerais.
Retry
Não repita toda consulta indiscriminadamente. Erros de conexão antes da execução podem ser candidatos; transações com resultado incerto exigem idempotência e verificação. Use a política de Retry com Backoff no Node.js apenas para erros classificados.
Observabilidade
Monitore:
- total, idle e waiting do pool;
- duração e taxa de erro por consulta;
- timeouts;
- transações abertas;
- conexões no PostgreSQL;
- locks e consultas lentas.
Não registre parâmetros sensíveis. Use nomes de operação e traces com OpenTelemetry no Node.js.
Como testar
Use banco isolado ou schema exclusivo por suíte. Teste concorrência, rollback, timeout, cliente não liberado, encerramento e falha de conexão. Não dependa da ordem dos testes nem compartilhe uma transação entre casos concorrentes.
Erros comuns
- Criar um pool por requisição: conexões se multiplicam.
- Esquecer release(): o pool esgota.
- Usar pool.query() dentro de transação: instruções podem usar clientes diferentes.
- Configurar max alto demais: o banco fica sobrecarregado.
- Não definir timeouts: consultas travadas ocupam clientes.
- Fechar pool antes do servidor: requisições em andamento falham.
- Registrar DATABASE_URL: credenciais vazam.
- Usar retry sem idempotência: operações são duplicadas.
Boas práticas para produção
- Crie um pool por processo.
- Use
pool.query()para consultas simples. - Libere clientes em
finally. - Use o mesmo cliente em transações.
- Dimensione pelo total de instâncias.
- Defina timeouts de conexão e consulta.
- Monitore fila e duração.
- Valide TLS e segredos.
- Feche o pool no graceful shutdown.
- Otimize consultas antes de aumentar o pool.
Conclusão
Um Pool PostgreSQL no Node.js reutiliza conexões e protege o banco contra criação descontrolada de sessões. pool.query() simplifica consultas individuais, enquanto clientes dedicados permitem transações consistentes.
O desempenho depende de limites, liberação e observabilidade. Dimensione o pool considerando todas as instâncias, aplique timeouts e feche recursos na ordem correta. Com essas práticas, a camada de banco permanece previsível mesmo sob concorrência.



