Todo serviço Node.js que se comunica com um banco de dados está, na verdade, executando dois sistemas simultaneamente: o próprio banco de dados e uma pequena peça de infraestrutura dentro do processo Node.js cuja única função é gerenciar como essa conexão é utilizada.
Esse segundo sistema - o pool de conexões - é fácil de ignorar porque um driver como pg ou um ORM como Prisma o esconde atrás de uma simples chamada .query(), mas quase todos os problemas reais de produção nesta seção (pools esgotados, tempestades de conexão fria em serverless, consultas N+1, vazamentos de transação) rastreiam de volta a como esse pool realmente se comporta.
Noções Básicas de Banco de Dados mostra o código para usar um pool corretamente; esta página é sobre por que o pooling existe e onde drivers, ORMs e o event loop se encaixam na arquitetura ao redor dele.
Um processo Node.js não abre uma nova conexão de banco de dados por consulta - ele mantém um pequeno conjunto reutilizado de conexões abertas (um pool) e empresta uma para a duração de cada consulta, porque abrir uma conexão é ordens de magnitude mais caro do que usar uma já aberta.
Por que Importa: O tamanho do pool, não a velocidade bruta do driver, é geralmente o limite de concorrência real em um serviço Node.js com banco de dados - o mal-entendido disso leva a requisições starved (pool muito pequeno) ou a um banco de dados sobrecarregado (pool muito grande, ou um por processo em uma implantação multi-instância).
Conceitos Chave:pool de conexões, driver, ORM, protocolo de rede, limite de transação, consulta N+1.
Quando Usar: Qualquer serviço Node.js emitindo mais do que algumas consultas por segundo, qualquer implantação serverless onde a rotatividade de conexões é o risco real, e qualquer base de código escolhendo entre um driver bruto e um ORM para um novo serviço.
Limitações / Compromissos: O pooling adiciona uma camada de estado (conexões emprestadas vs. ociosas, timeouts, vazamentos) que uma conexão ingênua por requisição não teria; ORMs adicionam conveniência e segurança de tipo ao custo de algum controle sobre o SQL exato que é executado.
Tópicos Relacionados: gerenciamento de transações, padrões de consulta N+1, limites de conexão serverless, o padrão de repositório, dimensionamento de pool de conexões.
Abrir uma conexão de banco de dados não é como abrir um arquivo - envolve um handshake TCP com o servidor de banco de dados, muitas vezes uma negociação TLS por cima disso, e então uma troca de autenticação antes que uma única consulta possa ser executada.
Toda essa sequência pode levar dezenas de milissegundos, o que é enorme em comparação com a própria consulta, então abrir uma conexão nova para cada requisição significaria pagar esse custo - e manter os próprios slots de conexão limitados do banco de dados abertos para ele - constantemente.
Um pool de conexões resolve isso da mesma forma que um ponto de táxi compartilhado resolve o problema de "comprar um carro para cada viagem": um número fixo de conexões é aberto uma vez, mantido ativo e distribuído para quem precisar de uma em seguida, retornado ao pool (não fechado) quando a consulta termina.
// Um pool por processo, criado uma vez - não uma conexão por requisiçãoconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });const { rows } = await pool.query("SELECT id FROM users WHERE id = $1", [id]);// pool.query() empresta uma conexão, executa a consulta e a retorna automaticamente
Acima do pool está um driver - pg para Postgres, o driver MongoDB, ioredis para Redis - uma biblioteca que fala o protocolo de rede real do banco de dados: o formato binário ou de texto específico que o servidor espera sobre o socket.
Um ORM (Prisma, Drizzle) é uma camada adicional sobre um driver, não um substituto para ele - ele ainda abre o mesmo tipo de conexões em pool através do driver subjacente, mas adiciona uma API com reconhecimento de esquema e tipada e gera o SQL (ou equivalente) para você em vez de você escrevê-lo manualmente.
O papel do event loop aqui é fácil de julgar mal: o modelo de I/O não bloqueante do Node significa que uma consulta não congela o processo enquanto espera a resposta do banco de dados, mas a conexão em si ainda é um canal único e ordenado - a maioria dos protocolos de rede de banco de dados processa uma requisição por vez por socket, então um pool de dez conexões realmente significa no máximo dez consultas em andamento ao mesmo tempo, não concorrência ilimitada.
É por isso que o tamanho do pool é um limite de concorrência que você escolhe, não um dial de desempenho para maximizar - um pool de max: 10 significa que a décima primeira requisição de consulta concorrente espera em uma fila por uma conexão para liberar, não importa quão rápido o próprio event loop esteja rodando.
requisição 1 ─┐
requisição 2 ─┼─ pool.query() ─▶ [pool: 10 conexões] ─▶ banco de dados
requisição 3 ─┘ ▲ 3 emprestadas, 7 ociosas
requisição 11 ── espera na fila ────┘ (todas as 10 atualmente emprestadas)
Dimensionar esse limite muito pequeno deixa o aplicativo starved sob carga - requisições enfileiram por uma conexão mesmo que o banco de dados em si tenha capacidade extra; dimensionar muito grande apenas move o gargalo para o servidor de banco de dados, que tem seu próprio limite rígido de conexões totais e degrada (ou recusa novas) uma vez que esse limite é atingido.
A matemática fica mais acentuada em uma implantação multi-instância: se o banco de dados permite 100 conexões no total e cinco instâncias Node cada uma abre um pool de max: 30, isso são 150 conexões possíveis contra um teto de 100 conexões - uma configuração que funciona bem com tráfego baixo e falha exatamente quando a carga aumenta o suficiente para cada instância buscar seu pool completo ao mesmo tempo.
Transações adicionam uma segunda camada de estado sobre o pooling: uma transação tem que rodar em uma conexão emprestada específica de BEGIN a COMMIT/ROLLBACK, porque o banco de dados rastreia o estado da transação por conexão, não por consulta - é por isso que o código de transação explicitamente pega um cliente emprestado e o passa adiante, em vez de chamar pool.query() para cada instrução dentro dele.
Ambientes Serverless levam o pooling de conexões ao seu ponto de ruptura, porque a suposição central do pool - um processo de longa duração que abre conexões uma vez e as reutiliza - não se sustenta quando cada invocação pode iniciar um novo processo com um pool vazio.
Em escala real, um surto de invocações serverless concorrentes pode cada uma tentar abrir seu próprio pequeno pool simultaneamente, produzindo um pico de novas conexões que pode exceder o limite total do banco de dados em segundos - a correção padrão é um pooler de conexão externo (PgBouncer, ou um equivalente gerenciado como o pooler do Supabase ou Neon) que fica entre o banco de dados e todas as instâncias serverless, multiplexando muitas conexões lógicas de aplicação em um número menor e estável de conexões reais de banco de dados. Ajuste de Pool de Conexões cobre essa matemática de dimensionamento e os padrões específicos de serverless em profundidade.
A escolha driver vs. ORM é realmente uma questão de onde você quer que o controle resida: um driver bruto dá visibilidade total sobre exatamente qual SQL é executado, ao custo de escrevê-lo e mantê-lo manualmente; um ORM gera esse SQL a partir de um esquema e um construtor de consultas tipado, trocando parte dessa visibilidade por velocidade de desenvolvimento e segurança em tempo de compilação - mas também pode esconder padrões de consulta caros por trás de código de aparência conveniente, mais notoriamente o problema de consulta N+1, onde buscar uma lista e depois buscar preguiçosamente os dados relacionados de cada item transforma uma consulta pretendida em N+1 viagens de ida e volta.
Nem um driver nem um ORM devem ser chamados diretamente de manipuladores de rota em qualquer coisa além de um serviço pequeno - o padrão comum é um repositório ou camada de acesso a dados sentada entre a lógica da aplicação e o cliente do banco de dados, para que a tecnologia de banco de dados (ou mesmo a escolha driver vs. ORM) possa mudar sem reescrever cada manipulador que toca nos dados. Padrão de Repositório cobre essa fronteira em detalhes.
Abordagem
Força
Fraqueza
Melhor Ajuste
Driver bruto (pg, driver MongoDB)
Controle total sobre consultas exatas; sobrecarga mínima de abstração
Você escreve e mantém SQL/consultas manualmente; menos segurança em tempo de compilação
Caminhos críticos de desempenho, SQL de relatórios complexos
ORM schema-first (Prisma)
Tipagem forte gerada a partir de um esquema, migrações integradas
Consultas geradas podem esconder padrões N+1; menos controle sobre SQL exato
APIs Greenfield priorizando velocidade de desenvolvimento e segurança de tipo
ORM SQL-first (Drizzle)
Construtor de consultas tipado que se mantém próximo ao SQL real
Ecossistema/maturidade menor que Prisma no momento
Equipes que desejam consultas tipadas sem perder a visibilidade em nível de SQL
Pooler externo (PgBouncer, poolers gerenciados)
Absorve tempestades de conexão; desacopla a contagem de instâncias de app dos limites de conexão do DB
Peça de infraestrutura adicionada; pooling em modo transação tem suas próprias ressalvas
Implantações serverless, frotas de alta contagem de instâncias
"O event loop torna as consultas de banco de dados totalmente paralelas." O event loop mantém o processo não bloqueante enquanto espera, mas cada conexão em pool ainda processa uma consulta por vez - a concorrência real é limitada pelo tamanho do pool, não por quantas consultas o event loop pode gerenciar.
"Um pool de conexões maior é sempre mais seguro." Passado um ponto, ele apenas transfere o gargalo para o teto de conexão do próprio servidor de banco de dados, que todas as instâncias de aplicação compartilham - pools superdimensionados em várias instâncias são uma causa comum de incidentes de "banco de dados recusando conexões".
"Um ORM substitui o driver e o pool." Ele fica sobre ambos - Prisma e Drizzle ainda abrem conexões em pool através de um driver subjacente; o ORM apenas muda como as consultas são escritas e tipadas, não a arquitetura de conexão por baixo.
"Serverless apenas precisa de um pool maior por instância para lidar com a carga." Mais conexões por instância piora o problema da tempestade de conexões, não melhora, porque cada invocação concorrente abre seu próprio pool - a correção é um pooler externo compartilhado, não um max maior por instância.
"Usar um ORM significa que não preciso pensar em consultas N+1." ORMs tornam os padrões N+1 mais fáceis de escrever acidentalmente, não impossíveis de escrever - o carregamento preguiçoso de relações dentro de um loop gera as mesmas viagens de ida e volta excessivas que um ORM deveria ajudar a evitar.
Por que o Node usa um pool de conexões em vez de uma conexão por requisição?
Abrir uma conexão de banco de dados envolve um handshake TCP, muitas vezes TLS, e autenticação - uma sequência lenta em comparação com a execução de uma consulta. Um pool abre um pequeno conjunto de conexões uma vez e as reutiliza, evitando esse custo em cada requisição.
Qual a diferença entre um driver de banco de dados e um ORM?
Um driver (como pg) fala diretamente o protocolo de rede do banco de dados e expõe uma API de consulta de baixo nível. Um ORM (como Prisma ou Drizzle) fica sobre um driver, gerando consultas a partir de um esquema ou construtor de consultas tipado em vez de você escrever SQL manualmente - ele não substitui o driver ou seu pool.
O I/O não bloqueante do Node significa que um pool de 10 conexões pode lidar com consultas concorrentes ilimitadas?
Não - a maioria dos protocolos de rede de banco de dados processa uma consulta por vez por conexão, então um pool de 10 realmente limita as consultas concorrentes "em andamento" a 10. O event loop mantém o processo livre enquanto espera, mas o pool limita quantas consultas podem estar "em andamento" contra o banco de dados simultaneamente.
Como devo decidir o que definir para o tamanho `max` de um pool?
Baseie-se na carga de consulta concorrente esperada por instância, dividida pelo número de instâncias que rodarão simultaneamente e pelo teto total de conexões do banco de dados - não na contagem de núcleos de CPU ou em um número padrão redondo. Ajuste de Pool de Conexões cobre a matemática de dimensionamento.
Por que as transações precisam de tratamento especial além de `pool.query()`?
O estado de uma transação (BEGIN até COMMIT/ROLLBACK) é rastreado por conexão física pelo banco de dados, não por consulta - então cada instrução em uma transação tem que rodar na mesma conexão emprestada, é por isso que o código de transação explicitamente pega um cliente emprestado em vez de chamar pool.query() repetidamente.
Por que serverless é especialmente difícil para pooling de conexões?
Um pool assume um processo de longa duração que abre conexões uma vez e as reutiliza em muitas requisições. Invocacões serverless podem iniciar novos processos com frequência, então um surto de invocações concorrentes pode cada uma abrir seu próprio pool ao mesmo tempo, produzindo um pico de conexões que pode exceder o limite do banco de dados.
O que exatamente é o problema de consulta N+1?
Buscar uma lista de N itens e depois buscar separadamente os dados relacionados de cada item um por um, transformando o que deveria ser uma ou duas consultas em N+1 viagens de ida e volta - é mais comum com carregamento de relação de ORM usado descuidadamente dentro de um loop.
O código da aplicação deve chamar o driver de banco de dados ou ORM diretamente?
Em qualquer coisa além de um serviço pequeno, não - um repositório ou camada de acesso a dados tipicamente fica entre os manipuladores de rota e o driver/ORM, para que a escolha de persistência possa mudar sem reescrever a lógica de negócios. Veja Padrão de Repositório.
O pooling de um banco de dados de documentos (MongoDB) é o mesmo de um relacional?
Sim, conceitualmente - o driver MongoDB também mantém um pool de conexões reutilizadas em vez de abrir uma por operação, pela mesma razão: a configuração da conexão é cara em relação à execução de uma operação.
Qual é o maior erro de iniciante com conexões de banco de dados em Node?
Criar uma nova conexão (ou um novo pool) por requisição em vez de um pool compartilhado por processo - isso anula todo o propósito do pooling e pode esgotar o limite de conexões do banco de dados sob carga mesmo moderada.