O pooling de conexões serverless é uma técnica que compartilha poucas conexões reais de banco entre muitas instâncias de função efêmeras. Ele existe porque dois modelos de escala discordam: a computação serverless responde à carga multiplicando instâncias, enquanto o banco trata cada conexão como um recurso caro, sustentado por memória e com teto rígido. Junte os dois de forma ingênua e um pico modesto de tráfego vira uma indisponibilidade com too many connections.
Principais pontos
| Pergunta | Resposta |
|---|---|
| A colisão | Funções escalam clonando; conexões têm teto e custam caro |
| O sintoma | FATAL: sorry, too many clients already durante os picos |
| A correção | Um pooler multiplexa muitos clientes sobre poucas conexões reais |
| O botão | Transaction vs. session pooling — compartilhamento vs. compatibilidade |
| A resposta do BaaS | Clientes falam HTTPS sem estado; a plataforma é dona do pool |
A falha, observada do lado do banco
-- O que o banco vê durante um pico de tráfego serverless:
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
-- active | 14
-- idle | 483 ← estacionadas por instâncias quentes
SHOW max_connections; -- 100 ← o teto que elas já estouraram
-- Cada conexão é um processo real segurando memória real —
-- por isso o teto existe, e por isso aumentá-lo não é a correção.
O formato do problema: cada instância de função abre a sua própria conexão, segura durante o ocioso entre invocações, e a contagem de instâncias é decidida pelo tráfego, não por você. O reúso acontece apenas dentro de uma instância quente — nunca entre instâncias —, então concorrência 300 significa 300 pools de uma conexão. Aumentar max_connections compra folga a um custo real de memória e perde para o próximo pico maior.
Num backend gerenciado, o código de aplicação sai dessa briga por completo — chamadas de SDK são requisições HTTPS sem estado, e nenhum cliente jamais segura uma conexão de banco:
// JavaScript / Node.js — Back4app JS SDK
// Inside a serverless function: no driver, no pool, no connection to leak.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
Parse.initialize('APP_ID', 'JS_KEY');
Parse.serverURL = 'https://parseapi.back4app.com';
export async function handler(event) {
const query = new Parse.Query('Order');
query.equalTo('status', 'pending');
query.limit(20);
const orders = await query.find(); // request returns; nothing stays open
return orders.map((o) => o.id);
}
// 1,000 concurrent invocations = 1,000 HTTP requests,
// not 1,000 database connections. // Flutter / Dart — Back4app Flutter SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
await Parse().initialize(
'APP_ID',
'https://parseapi.back4app.com',
clientKey: 'CLIENT_KEY',
);
Future<List<String>> pendingOrderIds() async {
final query = QueryBuilder<ParseObject>(ParseObject('Order'))
..whereEqualTo('status', 'pending')
..setLimit(20);
final response = await query.query(); // request returns; nothing stays open
return response.results!
.map((o) => (o as ParseObject).objectId!)
.toList();
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections. // iOS / Swift — Back4app Swift SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
ParseSwift.initialize(
applicationId: "APP_ID",
clientKey: "CLIENT_KEY",
serverURL: URL(string: "https://parseapi.back4app.com")!
)
let query = Order.query("status" == "pending")
.limit(20)
query.find { result in
if case .success(let orders) = result {
render(orders) // request returned; nothing stays open
}
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections. // Android / Kotlin — Back4app Android SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
Parse.initialize(
Parse.Configuration.Builder(context)
.applicationId("APP_ID")
.clientKey("CLIENT_KEY")
.server("https://parseapi.back4app.com")
.build()
)
val query = ParseQuery.getQuery<ParseObject>("Order")
query.whereEqualTo("status", "pending")
query.limit = 20
query.findInBackground { orders, e ->
if (e == null) render(orders) // request returned; nothing stays open
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections. Como o pooler absorve o fan-out
Um pooler como o PgBouncer aceita conexões de cliente aos milhares — cada uma barata do lado dele — e empresta conexões reais de servidor de um pool pequeno só quando chega trabalho. O banco enxerga vinte conexões calmas; quinhentas funções acreditam ter uma cada. Quando a demanda excede o pool, os clientes esperam milissegundos numa fila em vez de receber erro — convertendo uma falha dura em latência modesta.
O dimensionamento do pool é onde a intuição mais erra. O paralelismo útil de um banco é limitado por núcleos e disco — a vazão normalmente atinge o pico com um pool na casa das poucas dezenas, perto do número de núcleos vezes dois, e depois cai conforme mais conexões somam contenção. A aritmética do pooler funciona porque requisições serverless são curtas: uma conexão que atende uma transação de 8 ms pode atender bem mais de cem delas por segundo, então vinte conexões reais absorvem confortavelmente milhares de invocações de função. O pool não é um cache de conveniência; é um gargalo deliberado, colocado onde enfileirar é barato.
O botão decisivo é quanto tempo dura o empréstimo.
Transaction pooling vs. session pooling
| Dimensão | Session pooling | Transaction pooling |
|---|---|---|
| Duração do empréstimo | A sessão inteira do cliente | Uma transação, depois recolhida |
| Multiplicador de compartilhamento | Baixo — clientes ociosos estacionam conexões | Alto — ideal para trabalho serverless curto |
| Prepared statements | Totalmente suportados | Só com suporte em nível de protocolo, configurado explicitamente |
Estado de sessão (SET, tabelas temporárias, LISTEN/NOTIFY) | Funciona | Quebra — transações diferentes podem cair em conexões diferentes |
| Melhor para | Apps longevos, ferramentas de admin, migrações | Tráfego web e serverless de requisição/resposta |
O modo transação é o que torna cargas serverless viáveis — uma função que roda uma transação de 8 ms não deveria ser dona de uma conexão pelo seu tempo de vida quente de vários minutos —, mas ele não sai de graça: tudo que assume que “a minha conexão” persiste entre comandos vira um bug sutil. O conselho consagrado é transaction pooling para o tráfego de requisições e uma conexão direta ou por sessão para migrações e qualquer coisa com estado.
Casos de uso comuns
- Funções serverless e de edge batendo em SQL. O caso canônico — contagem de instâncias irregular contra um orçamento fixo de conexões.
- Muitos serviços pequenos, um banco. Vinte serviços × pools padrão de dez conexões = 200 conexões que ninguém planejou; um pooler compartilhado devolve a sanidade à aritmética.
- Plataformas multi-tenant. Cargas por tenant multiplicam a demanda por conexões; o pooling mantém o orçamento do banco compartilhado exequível.
- Rajadas de tráfego em bancos modestos. Picos de dia de lançamento enfileiram no pooler por milissegundos em vez de dar erro no banco.
- Proteger o primário durante incidentes. Um pool de tamanho fixo é um anteparo: clientes desgovernados esgotam a fila do pooler, não a memória do banco.
Você deveria rodar o seu próprio pooler? Matriz de decisão
| Rode um pooler quando… | Pule quando… |
|---|---|
| Funções ou muitos serviços conectam direto no SQL | Um BaaS gerenciado encerra o tráfego do cliente na camada de API |
| Erros de conexão aparecem em picos de carga | Tráfego estável de poucos servidores longevos com pool no cliente |
| Você opera o banco e o orçamento dele | A plataforma já faz pooling entre a camada de API e o banco |
| Tenants ou times dividem um mesmo banco | A carga é um app, um pool, bem dentro dos limites |
| Você consegue operar mais um componente crítico | Ninguém está de plantão para o pooler |
A assimetria honesta: um pooler resolve o esgotamento de conexões; um backend gerenciado o dissolve. Quando os clientes falam HTTPS com APIs geradas automaticamente, o fan-out nunca chega à camada de banco — o pooling continua acontecendo, mas como infraestrutura bem testada de outra pessoa, e não como componente seu.
Limitações e trade-offs
- Um pooler é um componente. Ele exige deploy, monitoramento, failover e upgrades — você trocou um problema de recurso por um problema de operação, num câmbio favorável mas não gratuito.
- O modo transação muda a semântica. Prepared statements, variáveis de sessão e advisory locks deixam de se comportar como documentado; as falhas são intermitentes e dependentes de carga — o pior tipo.
- Um salto a mais custa latência. Tipicamente abaixo de um milissegundo dentro da rede, mas está no caminho de toda consulta.
- O pooler pode ser o gargalo. Um pooler de thread única sob um pico violento enfileira fundo; o teto mudou de lugar, não sumiu.
- Pooling não conserta consulta lenta. Ele raciona conexões; uma consulta que segura o empréstimo por segundos mata o pool de fome. Consultas rápidas — e os índices por trás delas — continuam sendo a alavanca real de capacidade.
Pooling de conexões no Back4app
O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. A arquitetura dele responde à questão do pooling de forma estrutural: apps cliente e funções de Cloud Code emitem requisições HTTPS sem estado, e a camada de servidor da plataforma — não o seu código — mantém pools longevos e bem dimensionados até o banco gerenciado. O fan-out de instâncias do lado da computação nunca se traduz em fan-out de conexões do lado do banco, que é exatamente a propriedade que este artigo inteiro existe para projetar.
Perguntas frequentes
Por que o serverless esgota as conexões do banco de dados?
Porque dois modelos de escala colidem: computação serverless multiplica instâncias por requisição, enquanto o banco tem um orçamento fixo de conexões — muitas vezes em torno de 100 por padrão. Cada instância de função abre a sua própria conexão, segura durante o tempo ocioso, e um pico de tráfego que cria 500 instâncias pede 500 conexões de uma vez. O banco recusa o excedente, e consultas sem relação nenhuma começam a falhar.
O que um pooler como o PgBouncer faz na prática?
Ele fica entre o seu código e o banco, aceita milhares de conexões de cliente baratas e as multiplexa sobre um pool pequeno de conexões reais de servidor. Os clientes acham que têm uma conexão; na verdade pegam uma emprestada por uma transação ou sessão e devolvem. O banco enxerga um conjunto calmo e fixo de conexões, não importa quantas funções subam.
Qual a diferença entre transaction pooling e session pooling?
A duração do empréstimo. No session pooling, a conexão real fica com o cliente pela sessão inteira — seguro para qualquer recurso, mas um cliente ocioso ainda estaciona uma conexão. No transaction pooling, a conexão é emprestada só enquanto a transação roda e depois é recolhida, o que multiplica muito o compartilhamento mas quebra estado de sessão: prepared statements, variáveis de sessão e LISTEN/NOTIFY exigem cuidado ou falham.
Por que as conexões disparam quando as funções escalam?
Uma plataforma serverless trata concorrência clonando instâncias, e cada clone inicializa o seu próprio cliente de banco. O reúso de conexão só acontece dentro de uma instância quente, nunca entre instâncias. Então concorrência 300 significa 300 pools de tamanho um — o pior formato possível: todo o overhead do pooling sem nenhum compartilhamento. O fan-out é estrutural, não um bug no seu código.
Qual deve ser o tamanho do pool de conexões?
Bem menor do que a intuição sugere. A vazão costuma atingir o pico com um pool próximo do paralelismo real do banco — uma regra prática comum é o número de núcleos vezes dois, ajustado pelas esperas de disco — e não nas centenas. Além disso, conexões extras só somam contenção e custo de memória sem somar vazão. O trabalho do pooler é justamente fazer um pool pequeno parecer infinito para quem chama.
Ainda preciso de pooler usando um BaaS?
Não para o seu próprio tráfego — esse é o ponto. Um backend gerenciado encerra as requisições do cliente como chamadas HTTPS sem estado na camada de API; a camada de servidor dele mantém pools longevos e bem dimensionados até o banco. As suas funções e apps nunca seguram conexões de banco, então o cenário de esgotamento desaparece em vez de ser mitigado. Poolers só voltam à conversa se você conectar ferramentas externas direto no banco.
Quais as desvantagens dos poolers de conexão?
Eles somam um salto de rede em latência, viram mais um componente para operar e monitorar, e o modo transação muda a semântica em silêncio — recursos com escopo de sessão deixam de se comportar como documentado. Um único processo de pooler pode virar o gargalo sob carga irregular, e dimensioná-lo errado apenas move a fila de lugar. É infraestrutura essencial, mas é infraestrutura.