DEV Community

Lucas
Lucas

Posted on

Pool de Conexões com Banco de Dados: Por Que Reutilizar Conexões Importa

Toda aplicação que fala com um banco de dados relacional ou não relacional precisa, em algum momento, decidir como vai gerenciar suas conexões de rede. Essa decisão parece um detalhe de infraestrutura, mas costuma ser responsável por parte significativa da latência percebida pelo usuário final e por boa parte dos incidentes de indisponibilidade em produção.
Abrir e fechar uma conexão a cada consulta é um padrão comum em código iniciante, e funciona sem problemas quando o volume de requisições é baixo. O problema aparece quando a aplicação escala: o custo de estabelecer cada conexão se soma, o banco de dados começa a recusar novas conexões e a aplicação passa a travar sob carga que, tecnicamente, ela deveria suportar.
Este artigo explica por que manter conexões abertas e reutilizá-las através de um pool de conexões é uma prática recomendada, como esse mecanismo funciona internamente, quais parâmetros configurar e quais armadilhas evitar. A ideia é sair da leitura sabendo não apenas "usar um pool", mas entendendo o motivo técnico por trás de cada configuração.
O Que Realmente Acontece ao Abrir uma Conexão com o Banco
Uma conexão com um banco de dados não é uma operação instantânea. Ela envolve várias etapas que consomem tempo de rede e processamento, tanto do lado da aplicação quanto do lado do servidor de banco de dados:
Handshake TCP - três pacotes trocados entre cliente e servidor para estabelecer a conexão de rede.
Negociação TLS (quando a conexão é criptografada) - troca de certificados e chaves, um processo consideravelmente mais custoso que o handshake TCP puro.
Autenticação - envio de credenciais e validação no servidor de banco.
Inicialização de sessão - o banco aloca memória e estruturas internas para atender aquela conexão específica (buffers, contexto de transação, configurações de sessão).

Cada uma dessas etapas tem um custo em milissegundos. Isoladamente, pode parecer irrelevante. Multiplicado por centenas ou milhares de requisições por segundo, esse custo se torna uma fração significativa do tempo total de resposta da aplicação.
[SUGESTÃO VISUAL: diagrama de sequência mostrando as quatro etapas de estabelecimento de conexão (TCP, TLS, autenticação, inicialização de sessão) com o tempo aproximado de cada uma, comparado lado a lado com o fluxo de uma requisição que reutiliza uma conexão já existente]
Além do custo de latência, existe um custo de recursos no servidor. Bancos de dados relacionais tradicionais como PostgreSQL e MySQL alocam um processo ou thread por conexão ativa. Isso significa que cada conexão aberta consome memória no servidor, independentemente de estar sendo usada para executar uma consulta naquele instante.
Por Que Abrir e Fechar Conexão a Cada Requisição É um Problema
Um padrão frequente em código sem gestão de conexões é: abrir a conexão no início da requisição, executar a consulta e fechar a conexão no final. Esse padrão tem três consequências práticas:
Latência adicional em toda requisição. Se o handshake completo custa, por exemplo, entre 5 e 50 milissegundos dependendo da rede e do uso de TLS, esse tempo é somado ao tempo de execução da consulta em toda e qualquer requisição, mesmo nas mais simples.
Esgotamento do limite de conexões do banco. A maioria dos sistemas de banco de dados define um número máximo de conexões simultâneas (max_connections no PostgreSQL, por exemplo). Sob picos de tráfego, um padrão de abrir-fechar sem controle pode gerar mais tentativas de conexão simultâneas do que o banco suporta, resultando em erros de recusa de conexão justamente no momento de maior demanda.
Sobrecarga de CPU no servidor de banco. Autenticar e inicializar sessão repetidamente consome ciclos de CPU que poderiam estar sendo usados para processar consultas. Em cenários de alto volume, essa sobrecarga é mensurável e reduz a capacidade real de throughput do banco.
Vale diferenciar prática comum de recomendação técnica aqui: é comum ver esse padrão em protótipos e scripts pontuais, e nesse contexto ele não costuma causar dano. A recomendação de usar pool se aplica a aplicações que atendem requisições concorrentes de forma contínua, como APIs web, workers de fila e serviços de backend em geral.
O Que É um Pool de Conexões
Um pool de conexões é uma camada intermediária, geralmente uma biblioteca, que mantém um conjunto de conexões já estabelecidas com o banco de dados e as distribui sob demanda para a aplicação. Em vez de abrir uma conexão nova a cada operação, a aplicação "pega emprestada" uma conexão do pool, usa e devolve ao pool para que outra requisição a reutilize.
O fluxo típico funciona assim:
No início da aplicação, o pool abre um número mínimo de conexões e as mantém ociosas, prontas para uso.
Quando uma requisição precisa acessar o banco, ela solicita uma conexão ao pool.
Se houver conexão disponível, o pool a entrega imediatamente, sem custo de handshake.
Se não houver conexão disponível e o pool ainda não atingiu seu limite máximo, uma nova conexão é criada.
Se o limite máximo já foi atingido, a requisição aguarda em fila até que uma conexão seja liberada, respeitando um tempo limite configurável.
Ao terminar de usar a conexão, a aplicação a devolve ao pool em vez de encerrá-la.

[SUGESTÃO VISUAL: diagrama de arquitetura mostrando múltiplas instâncias da aplicação, cada uma com seu pool de conexões, conectadas a um único servidor de banco de dados, destacando o número de conexões ociosas versus ativas em cada pool]
Esse mecanismo resolve diretamente os três problemas descritos na seção anterior: elimina o custo repetido de handshake para a maioria das requisições, impõe um teto controlado sobre o número de conexões simultâneas com o banco e reduz a carga de autenticação no servidor.
Parâmetros Essenciais de um Pool de Conexões
Configurar um pool de conexões não é uma decisão de "ligar e esquecer". Os parâmetros abaixo determinam diretamente o comportamento da aplicação sob carga.
Tamanho mínimo e máximo do pool
O tamanho mínimo define quantas conexões ficam sempre abertas, mesmo sem tráfego. O tamanho máximo define o teto de conexões simultâneas que aquela instância da aplicação pode manter com o banco.
Um erro comum é definir o tamanho máximo com base apenas na capacidade da aplicação, sem considerar que múltiplas instâncias da mesma aplicação (em um cluster, por exemplo) multiplicam esse número. Se a aplicação roda em dez réplicas e cada uma configura um pool de cinquenta conexões, o banco pode receber até quinhentas conexões simultâneas - um número que facilmente ultrapassa o max_connections de instâncias de banco de médio porte.
Uma regra prática, não uma regra absoluta, é calcular o tamanho máximo do pool considerando o número de réplicas da aplicação e reservar margem para outras conexões administrativas e ferramentas de monitoramento.
Tempo de espera por conexão (connection timeout)
Define quanto tempo uma requisição aguarda por uma conexão disponível antes de falhar com erro. Um valor muito alto faz a aplicação parecer travada sob picos de carga; um valor muito baixo gera falhas desnecessárias em picos passageiros e legítimos.
Tempo de vida máximo da conexão (max lifetime)
Mesmo conexões saudáveis se beneficiam de serem recicladas periodicamente. Isso evita problemas relacionados a balanceadores de carga com afinidade de sessão, atualizações de configuração do banco que exigem reconexão e vazamentos sutis de memória em drivers de longa duração.
Tempo de ociosidade máximo (idle timeout)
Conexões que ficam muito tempo sem uso são fechadas automaticamente, liberando recursos tanto do lado da aplicação quanto do banco. Isso é especialmente relevante em aplicações com tráfego variável ao longo do dia.
Teste de validação da conexão
Antes de entregar uma conexão à aplicação, muitos pools executam uma consulta simples (como SELECT 1) para confirmar que a conexão ainda está ativa. Isso evita que a aplicação receba uma conexão que o banco já encerrou por timeout, o que geraria um erro na primeira tentativa de uso.
Sem Pool Versus Com Pool: Comparação Direta
Aspecto Sem pool de conexões Com pool de conexões Latência por requisição Inclui custo de handshake em toda chamada Custo de handshake amortizado entre muitas requisições Uso de conexões no banco Tende a crescer sem controle sob carga Limitado a um teto configurável Comportamento sob pico de tráfego Risco de recusa de conexão pelo banco Fila controlada com timeout previsível Consumo de CPU no banco Maior, por autenticação repetida Menor, autenticação ocorre com menos frequência Complexidade de configuração Baixa Exige ajuste de parâmetros (tamanho, timeouts) Previsibilidade em produção Baixa sob alta concorrência Alta, com comportamento configurável
Exemplos de Configuração em Node.js
Os exemplos a seguir são ilustrativos e servem para mostrar como os conceitos descritos acima se traduzem em código real no ecossistema Node.js. Ajuste os valores de acordo com o perfil real de tráfego e a capacidade do banco de dados utilizado. Os exemplos assumem Node.js 18 ou superior e uso de TypeScript, mas a lógica é idêntica em JavaScript puro.
Configurando o pool com pg (PostgreSQL)

Dependencias
npm install pg
npm install --save-dev @types/pg
// db/pool.ts
import { Pool } from "pg";
export const pool = new Pool({
host: process.env.DB_HOST,
port: 5432,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
max: 20, // número máximo de conexões simultâneas no pool
min: 5, // conexões mantidas ociosas, prontas para uso
idleTimeoutMillis: 30000, // fecha conexões ociosas após 30s
connectionTimeoutMillis: 5000, // tempo máximo de espera por uma conexão livre
maxLifetimeSeconds: 1800, // recicla conexões após 30 minutos, mesmo em uso
});
// Encerra o pool de forma controlada ao desligar a aplicação
process.on("SIGTERM", async () => {
await pool.end();
});
Erro comum: instanciar um novo Pool dentro de uma função de rota ou de um handler de requisição. Isso cria um pool novo a cada chamada, anulando completamente o benefício da reutilização de conexões. O pool deve ser criado uma única vez, no início da aplicação, e reutilizado em todo o ciclo de vida do processo.
Executando consultas com liberação segura da conexão
// repositories/usuarioRepository.ts
import { pool } from "../db/pool";
export async function buscarUsuarioPorId(id: string) {
const client = await pool.connect();
try {
const resultado = await client.query(
"SELECT id, nome, email FROM usuarios WHERE id = $1",
[id]
);
return resultado.rows[0] ?? null;
} finally {
client.release(); // devolve a conexão ao pool, não a encerra
}
}
Erro comum: esquecer de chamar client.release() em algum caminho de código, especialmente quando uma exceção é lançada antes da liberação. Isso causa vazamento de conexão (connection leak): o pool gradualmente fica sem conexões disponíveis até que novas requisições passem a falhar por timeout. Sempre libere a conexão dentro de um bloco finally, mesmo quando a consulta falha.
Para consultas simples e isoladas, sem necessidade de controlar transação manualmente, o próprio pool expõe um atalho que já cuida da liberação internamente:
export async function contarUsuariosAtivos() {
const resultado = await pool.query(
"SELECT COUNT(*) FROM usuarios WHERE ativo = true"
);
return Number(resultado.rows[0].count);
}
Nesse caso, pool.query obtém uma conexão, executa a consulta e a devolve automaticamente, mesmo em caso de erro. É a forma recomendada sempre que não houver necessidade de múltiplas instruções na mesma transação.
Transações mantendo a mesma conexão
Quando uma operação envolve múltiplas instruções que precisam ser tratadas como uma unidade atômica, é necessário reter a mesma conexão do início ao fim da transação, e não deixar o pool distribuí-la para outra requisição no meio do processo:
export async function transferirSaldo(
contaOrigemId: string,
contaDestinoId: string,
valor: number
) {
const client = await pool.connect();
try {
await client.query("BEGIN");
await client.query(
"UPDATE contas SET saldo = saldo - $1 WHERE id = $2",
[valor, contaOrigemId]
);
await client.query(
"UPDATE contas SET saldo = saldo + $1 WHERE id = $2",
[valor, contaDestinoId]
);
await client.query("COMMIT");
} catch (erro) {
await client.query("ROLLBACK");
throw erro;
} finally {
client.release();
}
}
Erro comum: fazer chamadas de rede externas (uma requisição HTTP a outro serviço, por exemplo) entre o BEGIN e o COMMIT. Isso prende a conexão do pool pelo tempo total dessa chamada externa, reduzindo a disponibilidade de conexões para outras requisições. Transações devem conter apenas operações de banco de dados, e o mais curtas possível.
Monitorando o estado do pool
O objeto Pool do pg expõe propriedades úteis para observabilidade, que podem ser expostas em um endpoint de métricas ou enviadas a uma ferramenta de monitoramento:
export function obterMetricasDoPool() {
return {
totalConexoes: pool.totalCount, // conexões abertas no momento
conexoesOciosas: pool.idleCount, // conexões livres, prontas para uso
requisicoesEmEspera: pool.waitingCount, // requisições aguardando conexão livre
};
}
Um valor de waitingCount consistentemente acima de zero é um indício de que o pool está subdimensionado para o volume de tráfego atual, e não apenas um pico passageiro.
[SUGESTÃO VISUAL: captura de tela de um painel de monitoramento (Grafana ou similar) mostrando as métricas totalCount, idleCount e waitingCount do pool ao longo do tempo, com um pico de waitingCount coincidindo com um horário de alto tráfego]
Usando um ORM: Prisma como alternativa de mais alto nível
Para times que preferem não escrever SQL diretamente, ORMs como o Prisma gerenciam o pool de conexões internamente, mas ainda expõem parâmetros de configuração relevantes na connection string:
DATABASE_URL="postgresql://usuario:senha@host:5432/banco?connection_limit=20&pool_timeout=10"
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export async function buscarUsuarioPorId(id: string) {
return prisma.usuario.findUnique({ where: { id } });
}
O parâmetro connection_limit corresponde ao tamanho máximo do pool, e pool_timeout ao tempo de espera por uma conexão livre, descritos anteriormente. A diferença central para o exemplo com pg é que o Prisma abstrai a obtenção e liberação manual da conexão, reduzindo o risco de vazamento por esquecimento de release(), ao custo de menor controle fino sobre o comportamento do pool.
[FONTE EXTERNA: documentação oficial do Prisma sobre gerenciamento de pool de conexões e parâmetros de connection string]
Quando Não Usar (ou Repensar) um Pool de Conexões
Nem todo cenário se beneficia de um pool tradicional de conexões persistentes. Vale reconhecer as limitações:
Ambientes serverless com escala elástica agressiva. Funções serverless em Node.js (como AWS Lambda) podem escalar para centenas de instâncias simultâneas, e cada instância fria (cold start) tende a criar seu próprio Pool do zero. Isso pode gerar, na prática, mais conexões simultâneas com o banco do que em uma arquitetura tradicional com servidor de longa duração, anulando o benefício do pool. Nesses casos, a prática recomendada costuma envolver um pooler externo compartilhado entre as instâncias, como o PgBouncer para PostgreSQL, ou serviços de proxy de conexão oferecidos pelo próprio provedor de nuvem, em vez de um pool interno por instância de função.
Cargas de trabalho batch de curtíssima duração. Scripts que executam uma única operação e encerram não se beneficiam de manter conexões ociosas esperando reuso.
Bancos de dados que já gerenciam conexão de forma diferente. Alguns bancos NoSQL e serviços gerenciados possuem seus próprios mecanismos de multiplexação de conexão no lado do cliente, e adicionar uma camada extra de pool pode ser redundante ou até conflitante. Vale consultar a documentação oficial do driver específico antes de assumir que o mesmo modelo se aplica.
Erros Comuns ao Trabalhar com Pool de Conexões
Vazamento de conexão (connection leak). Já mencionado acima: ocorre quando a aplicação obtém uma conexão do pool e não a devolve, geralmente por um caminho de exceção não tratado. O sintoma característico é o pool ficando gradualmente esgotado até que novas requisições comecem a falhar por timeout, mesmo com carga estável.
Pool subdimensionado. Um pool muito pequeno para o volume real de requisições concorrentes gera fila constante, aumentando a latência percebida mesmo quando o banco tem capacidade de sobra.
Pool superdimensionado. Um pool grande demais em relação à capacidade do banco pode, paradoxalmente, reduzir o desempenho: o servidor de banco gasta mais tempo alternando entre um número excessivo de conexões ativas do que processando consultas.
Transações longas segurando conexões. Manter uma transação aberta por muito tempo (aguardando uma chamada externa, por exemplo) prende uma conexão do pool que poderia estar atendendo outras requisições. Transações devem ser mantidas o mais curtas possível.
Ignorar múltiplas instâncias da aplicação no cálculo do tamanho do pool. Como discutido na seção de parâmetros, o tamanho do pool deve ser pensado em conjunto com o número de réplicas da aplicação, não isoladamente.
Boas Práticas ao Configurar um Pool de Conexões
Defina o tamanho máximo do pool considerando o número de réplicas da aplicação e a capacidade real do banco, não apenas a demanda teórica de uma única instância.
Sempre libere a conexão explicitamente em um bloco de finalização (finally, using, context manager, dependendo da linguagem).
Configure tempo de vida máximo da conexão para evitar problemas com balanceadores de carga e reconfigurações do banco.
Monitore métricas do pool em produção: conexões ativas, conexões ociosas, tempo médio de espera por conexão e número de timeouts.
Mantenha transações curtas e evite operações de rede externas (chamadas HTTP, por exemplo) dentro de uma transação de banco de dados.
Em ambientes com múltiplas instâncias ou serverless, avalie um pooler externo compartilhado em vez de múltiplos pools internos isolados.

Checklist Rápido
[ ] O tamanho máximo do pool foi calculado considerando todas as réplicas da aplicação
[ ] Existe timeout configurado para espera por conexão disponível
[ ] Existe tempo de vida máximo definido para reciclagem de conexões
[ ] Todas as conexões obtidas são devolvidas ao pool em blocos de finalização, inclusive em caminhos de erro
[ ] Transações são curtas e não incluem chamadas de rede externas
[ ] Métricas do pool (ativas, ociosas, timeouts) estão sendo monitoradas em produção
[ ] Ambientes serverless ou de escala elástica foram avaliados quanto à necessidade de um pooler externo

Perguntas Frequentes
Um pool de conexões substitui a necessidade de otimizar consultas SQL? Não. O pool reduz o custo de estabelecer conexão, mas não tem relação com o tempo de execução da consulta em si. Uma consulta mal otimizada continua lenta independentemente de a conexão ser reutilizada ou não; os dois problemas são independentes e ambos merecem atenção.
É possível ter um pool de conexões grande demais? Sim. Um pool maior que a capacidade real do banco pode aumentar a contenção de recursos no servidor, já que cada conexão ativa consome memória e ciclos de processamento independentemente do volume de trabalho útil realizado.
Aplicações serverless devem sempre usar um pool interno? Não necessariamente. Em cenários de escala elástica agressiva, um pooler externo compartilhado costuma ser mais adequado do que um pool interno por instância, já que evita a multiplicação do número de conexões simultâneas com o banco.
Como saber se o tamanho do pool está adequado? Acompanhando métricas como tempo médio de espera por conexão, taxa de timeouts e número de conexões ativas versus ociosas ao longo do tempo. Um pool bem dimensionado raramente força requisições a esperar por conexão disponível sob carga normal.
Bancos de dados NoSQL também se beneficiam de pool de conexões? Depende do driver e do banco específico. Muitos drivers de bancos NoSQL já implementam multiplexação de conexão internamente, e é recomendável consultar a documentação oficial do driver antes de adicionar uma camada extra de pool.
O que causa o erro "too many connections" no banco de dados? Geralmente indica que o número de conexões simultâneas abertas pela aplicação (ou pela soma de todas as réplicas) ultrapassou o limite configurado no servidor de banco. Ajustar o tamanho do pool, reduzir o número de réplicas ou aumentar o limite do banco são possíveis caminhos, cada um com trade-offs próprios de custo e capacidade.
Fechar a conexão manualmente após cada uso é sempre errado? Não é uma regra absoluta. Em scripts pontuais, jobs batch de curta duração ou ferramentas administrativas executadas raramente, abrir e fechar conexão sem pool é uma prática aceitável. A recomendação de pool se aplica principalmente a aplicações que atendem requisições concorrentes de forma contínua.
Conclusão
Manter conexões abertas e reutilizá-las através de um pool de conexões reduz a latência de cada requisição, evita o esgotamento do limite de conexões do banco de dados e diminui a carga de autenticação repetida no servidor. O ganho não é apenas teórico: ele se manifesta diretamente em picos de tráfego, justamente o momento em que a estabilidade da aplicação mais importa.
Configurar um pool de conexões corretamente exige entender os parâmetros que o governam - tamanho mínimo e máximo, timeout de espera, tempo de vida máximo e tempo de ociosidade - e evitar armadilhas conhecidas como vazamento de conexão e dimensionamento que ignora o número de réplicas da aplicação.
Se a aplicação em questão ainda abre e fecha conexões manualmente a cada operação, avaliar a adoção de um pool de conexões é um dos ajustes de infraestrutura com melhor relação entre esforço de implementação e impacto em estabilidade e desempenho.

Top comments (0)