Se o projeto usa SQLx, escolha primeiro o pool nativo do SQLx; se usa tokio-postgres, Redis ou recursos heterogêneos, escolha Deadpool pela variedade de adaptadores e configuração prática; use bb8 quando você quer uma abstração enxuta baseada em ManageConnection ou quando o driver já oferece integração bb8. O critério decisivo não é um benchmark isolado: é compatibilidade com o driver, política de reciclagem, timeout de aquisição, observabilidade e comportamento sob saturação.
Um pool de conexões parece detalhe de infraestrutura até a API receber mais requisições do que o banco suporta. Nesse momento, a diferença entre “aguardar com limite” e “criar conexões sem controle” vira latência, erro 5xx ou indisponibilidade. Este guia compara Deadpool vs bb8, mostra exemplos com PostgreSQL e Redis e explica quando evitar ambos em favor do SQLx.
Resposta rápida: Deadpool, bb8 ou SQLx Pool?
| Situação | Escolha inicial | Por quê |
|---|---|---|
| Aplicação já usa SQLx | sqlx::Pool | integração nativa com queries, transactions e drivers |
tokio-postgres com configuração pronta | deadpool-postgres | adapter consolidado e reciclagem configurável |
| Redis assíncrono | deadpool-redis | integração direta e ergonomia consistente |
Driver fornece bb8::ManageConnection | bb8 | abstração pequena e familiar |
| Recurso customizado reutilizável | Deadpool ou bb8 | escolha pela API de manager e pelas métricas necessárias |
| SQLite local simples | frequentemente pool nativo do driver | limite de escrita e modelo do SQLite importam mais que a marca do pool |
| Uma única conexão em CLI curta | nenhum pool | pooling adiciona complexidade sem ganho real |
A regra de arquitetura é: use o pool mais próximo do driver. Um wrapper extra cria mais estados, tipos de erro e pontos de configuração. Só troque quando houver uma necessidade concreta que o pool nativo não atende.
O que um pool realmente faz
Abrir uma conexão com banco ou serviço remoto pode envolver DNS, TCP, TLS, autenticação e negociação de sessão. Repetir isso em toda requisição desperdiça tempo e recursos. O pool mantém um conjunto limitado de conexões prontas e executa um ciclo:
- uma task solicita um recurso;
- o pool entrega uma conexão ociosa ou cria uma até o limite;
- a task usa a conexão;
- ao liberar o guard, a conexão retorna ao pool;
- antes de reutilizá-la, o pool pode validar ou reciclar o recurso;
- se o limite está ocupado, a task aguarda — idealmente com timeout.
O pool não aumenta magicamente a capacidade do PostgreSQL. Ele impõe orçamento. Se o banco aguenta 40 conexões da aplicação e você inicia quatro réplicas, configurar 40 por réplica pode abrir 160 conexões. O cálculo precisa considerar todas as instâncias, migrações, ferramentas administrativas e margem de segurança.
Deadpool: adapters prontos e recursos assíncronos
Deadpool é uma família de crates para gerenciar objetos reutilizáveis em aplicações assíncronas. O crate base oferece a abstração; crates como deadpool-postgres e deadpool-redis conectam o pool a clientes concretos.
Ele costuma encaixar bem quando:
- o projeto usa
tokio-postgresdiretamente; - há PostgreSQL e Redis no mesmo serviço;
- você quer configuração serializável e factories por adapter;
- a política de reciclagem precisa de ajuste;
- o time prefere uma API de pool consistente entre recursos diferentes.
Exemplo com deadpool-postgres
O Cargo.toml varia conforme as versões estáveis e os recursos TLS escolhidos. A estrutura típica é:
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio-postgres = "0.7"
deadpool-postgres = "0.14"
Criação e uso do pool:
use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};
use tokio_postgres::{Config, NoTls};
fn criar_pool(database_url: &str) -> Result<Pool, Box<dyn std::error::Error>> {
let pg_config: Config = database_url.parse()?;
let manager = Manager::from_config(
pg_config,
NoTls,
ManagerConfig {
recycling_method: RecyclingMethod::Fast,
},
);
let pool = Pool::builder(manager)
.max_size(16)
.build()?;
Ok(pool)
}
async fn buscar_nome(
pool: &Pool,
usuario_id: i64,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let client = pool.get().await?;
let row = client
.query_opt(
"select nome from usuarios where id = $1",
&[&usuario_id],
)
.await?;
Ok(row.map(|r| r.get("nome")))
}
NoTls simplifica o exemplo, mas não é recomendação universal. Em produção, configure TLS de acordo com o provedor e valide certificados. O ponto importante é que o valor retornado por pool.get() controla o empréstimo; quando sai de escopo, a conexão pode voltar ao pool.
Reciclagem não é detalhe cosmético
Uma conexão pode continuar aberta no cliente e já estar inválida no servidor. Reinícios, proxies, timeouts de rede e mudanças de credencial criam conexões zumbis. A política de reciclagem decide quanto trabalho fazer antes de entregar o recurso novamente.
- Validação mais leve: menor overhead, mas o primeiro comando pode descobrir uma conexão quebrada.
- Validação por query: mais confiança antes do uso, ao custo de uma ida adicional ao banco.
- Recriação após erro: remove o recurso defeituoso e abre outro dentro do orçamento.
Não copie uma política sem medir. Uma query de validação em toda aquisição pode multiplicar tráfego; validação leve demais pode elevar erros logo após failover.
bb8: uma abstração pequena com ManageConnection
bb8 modela o pool em torno de um manager que sabe:
- criar uma conexão;
- verificar se ela continua válida;
- decidir se ela está quebrada.
Essa separação deixa a biblioteca genérica e torna o contrato explícito. bb8 é uma boa escolha quando a stack já possui um adapter, como bb8-postgres, ou quando você quer implementar um manager pequeno para um protocolo interno.
Exemplo com bb8-postgres
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio-postgres = "0.7"
bb8 = "0.8"
bb8-postgres = "0.8"
use bb8::Pool;
use bb8_postgres::PostgresConnectionManager;
use tokio_postgres::NoTls;
type PgPool = Pool<PostgresConnectionManager<NoTls>>;
async fn criar_pool(
database_url: &str,
) -> Result<PgPool, Box<dyn std::error::Error>> {
let manager = PostgresConnectionManager::new_from_stringlike(
database_url,
NoTls,
)?;
let pool = Pool::builder()
.max_size(16)
.build(manager)
.await?;
Ok(pool)
}
async fn buscar_email(
pool: &PgPool,
usuario_id: i64,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let conn = pool.get().await?;
let row = conn
.query_opt(
"select email from usuarios where id = $1",
&[&usuario_id],
)
.await?;
Ok(row.map(|r| r.get("email")))
}
A experiência no handler é parecida com Deadpool: adquirir, executar e liberar. A diferença aparece mais na configuração, no contrato do manager, nos adapters disponíveis e no modo como o projeto instrumenta estado e erros.
Deadpool vs bb8: comparação prática
| Critério | Deadpool | bb8 |
|---|---|---|
| Abstração central | manager + objetos gerenciados | ManageConnection |
| PostgreSQL | deadpool-postgres | bb8-postgres |
| Redis | adapter dedicado popular | depende do adapter escolhido |
| Recursos não-SQL | um dos focos do ecossistema | possível com manager customizado |
| Curva inicial | simples com adapters prontos | simples quando já existe manager |
| Reciclagem | políticas e hooks do adapter | validação/quebra no manager |
| Melhor argumento | consistência entre recursos | contrato genérico enxuto |
| Razão ruim para escolher | “parece mais rápido” sem carga real | “tem menos código” ignorando operação |
Em aplicações normais, a latência da query e da rede domina diferenças minúsculas de bookkeeping do pool. Compare sob o workload real: quantidade de tasks, duração das queries, p95 de espera, falhas de conexão e comportamento durante restart do banco.
Quando o pool nativo do SQLx é a resposta
Se o projeto já usa SQLx, PgPool, MySqlPool e SqlitePool integram aquisição, executors e transactions. O caminho idiomático é:
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;
async fn criar_pool(database_url: &str) -> Result<sqlx::PgPool, sqlx::Error> {
PgPoolOptions::new()
.max_connections(16)
.min_connections(2)
.acquire_timeout(Duration::from_secs(2))
.idle_timeout(Duration::from_secs(600))
.connect(database_url)
.await
}
Você ganha um único modelo de erro e integração direta com query!, migrations e transactions. Colocar Deadpool ou bb8 em volta de SQLx normalmente duplica responsabilidade.
Use outro pool apenas se ele estiver gerenciando outro tipo de recurso. Exemplo: SQLx para PostgreSQL e deadpool-redis para Redis é uma combinação coerente; SQLx dentro de bb8 dentro de um state Axum raramente é.
Para uma comparação de acesso a dados mais ampla, veja Diesel vs SQLx e o guia de migrations SQLx com PostgreSQL.
Redis com Deadpool: o pool também vale fora do SQL
Redis é um caso comum porque conexões e multiplexação dependem do cliente e do padrão de carga. Com deadpool-redis, a estrutura costuma ser:
use deadpool_redis::{Config, Pool, Runtime};
use deadpool_redis::redis::AsyncCommands;
fn criar_pool_redis(url: &str) -> Result<Pool, deadpool_redis::CreatePoolError> {
let config = Config::from_url(url);
config.create_pool(Some(Runtime::Tokio1))
}
async fn buscar_cache(
pool: &Pool,
chave: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let mut conn = pool.get().await?;
let valor: Option<String> = conn.get(chave).await?;
Ok(valor)
}
Antes de adotar um pool Redis, confira se o cliente e o modo escolhido já oferecem multiplexação adequada. Pooling não é reflexo automático: ele é útil quando a concorrência, o protocolo e as garantias do cliente justificam múltiplos recursos.
O guia de Redis como cache em backends Rust cobre TTL, stampede, invalidação e fallback — problemas que um pool sozinho não resolve.
Integração com Axum sem espalhar o pool
O pool deve entrar no state da aplicação e permanecer na borda da infraestrutura. Handlers podem chamar um repositório; regras de negócio não precisam conhecer deadpool_postgres::Pool.
use axum::{extract::State, http::StatusCode, Json};
use serde::Serialize;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
usuarios: Arc<dyn UsuarioRepository>,
}
#[derive(Serialize)]
struct UsuarioDto {
id: i64,
nome: String,
}
#[axum::async_trait]
trait UsuarioRepository: Send + Sync {
async fn por_id(&self, id: i64) -> anyhow::Result<Option<UsuarioDto>>;
}
async fn obter_usuario(
State(state): State<AppState>,
axum::extract::Path(id): axum::extract::Path<i64>,
) -> Result<Json<UsuarioDto>, StatusCode> {
state
.usuarios
.por_id(id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
A implementação concreta guarda Deadpool, bb8 ou SQLx. Essa fronteira facilita testes e evita uma migração dolorosa se o acesso a dados mudar. Para o restante da stack web, continue pelo tutorial de API REST com Axum e pelo guia de serviços resilientes com Tower e Axum.
Timeout, backpressure e saturação
Um pool cheio cria uma fila invisível de tasks aguardando. Sem timeout, a latência cresce até o balanceador desistir. Sem limite externo, milhares de tasks podem consumir memória enquanto esperam 16 conexões.
Uma política robusta combina:
max_sizecoerente com a capacidade do banco;- timeout curto de aquisição, abaixo do timeout HTTP total;
- limite de concorrência antes do handler ou repositório;
- timeout de query no servidor/cliente;
- cancelamento quando a requisição do usuário desaparece;
- resposta diferenciada para saturação temporária, sem vazar detalhes.
Exemplo de timeout em torno da aquisição quando a API do pool/projeto não o aplica diretamente:
use std::time::Duration;
let conn = tokio::time::timeout(
Duration::from_secs(2),
pool.get(),
)
.await
.map_err(|_| AppError::PoolSaturado)??;
Não segure a conexão enquanto faz chamada HTTP externa, espera fila ou processa um arquivo. Adquira o mais tarde possível e libere logo após a transaction/query. Uma conexão presa durante trabalho não relacionado reduz throughput do sistema inteiro.
Observabilidade: meça a espera, não apenas a query
Duas latências importam:
- tempo de aquisição: quanto uma task esperou pelo pool;
- tempo de operação: quanto a query/comando levou após obter conexão.
Se você mede só a query, uma requisição pode parecer ter executado SQL em 20 ms enquanto ficou 1,8 s esperando uma conexão. Registre:
- conexões em uso e ociosas;
- tamanho máximo configurado;
- aquisições bem-sucedidas;
- timeouts de aquisição;
- erros de criação/reciclagem;
- histograma de espera;
- histograma de query por operação normalizada.
Não use SQL completo, ID de usuário ou URL de banco como label. O guia de Prometheus e métricas em Rust mostra como evitar cardinalidade explosiva; tracing ajuda a correlacionar a espera do pool com a requisição.
Como dimensionar o pool
Não existe fórmula universal, mas existe método:
- descubra o limite seguro de conexões do banco;
- reserve conexões para administração, migrations e jobs;
- divida o restante pelo máximo de réplicas da aplicação;
- comece conservador;
- rode carga representativa;
- observe espera, CPU do banco, locks, I/O e p95/p99;
- aumente apenas se o banco tem folga e a espera é realmente o gargalo.
Exemplo: PostgreSQL aceita 100 conexões; você reserva 20; pode ter até quatro réplicas. O teto teórico seria 20 por réplica, mas começar com 12–16 deixa margem para deploy rolling e variação operacional.
Mais conexões podem piorar performance ao aumentar contenção, memória e concorrência de disco. Queries lentas, índices ausentes e transactions longas não são corrigidos com max_size = 200.
Testes que valem mais que um benchmark sintético
Esgotamento
Configure pool de tamanho 2, segure duas conexões e confirme que a terceira aquisição expira no tempo esperado.
Conexão inválida
Reinicie o banco ou encerre uma sessão e verifique se o pool descarta/recicla o recurso sem envenenar requisições seguintes.
Transaction longa
Force uma operação lenta e confirme que o limite de concorrência protege o restante da API.
Shutdown
Interrompa o serviço durante carga e valide graceful shutdown: novas requisições param, operações em voo encerram dentro do prazo e recursos são liberados.
Réplicas
Teste o total agregado. Um container local não revela o que acontece quando seis pods multiplicam o pool.
Combine esses testes com as estratégias de testes em Rust e instrumente o cenário antes de comparar bibliotecas.
Erros comuns
Escolher por stars ou microbenchmark
Adapters, manutenção e comportamento de falha pesam mais que nanossegundos no caminho feliz.
Não configurar timeout de aquisição
A fila cresce e transforma saturação breve em cascata de timeouts.
Abrir um pool por requisição
O pool deve ser criado no bootstrap e compartilhado. Criá-lo por handler elimina o benefício e pode atacar o banco com novas conexões.
Manter conexão durante trabalho externo
Chamada a API, renderização, upload e processamento CPU-bound não deveriam prender uma conexão ociosa.
Ignorar o número de réplicas
max_size é por processo. Autoscaling multiplica conexões.
Tratar pool como cache de resultado
Pool reutiliza conexões; cache reutiliza dados. São problemas diferentes.
Misturar três pools para o mesmo driver
Padronize por datastore. SQLx Pool para PostgreSQL e Deadpool para Redis pode fazer sentido; dois pools concorrentes para o mesmo PostgreSQL quase nunca fazem.
Checklist de decisão para produção
- O driver já possui pool nativo bem integrado?
- Existe adapter mantido para Deadpool ou bb8?
- O pool suporta timeout de aquisição e limites explícitos?
- A reciclagem funciona após restart/failover?
- As métricas mostram espera, uso e erro?
- O tamanho considera todas as réplicas?
- Transactions são curtas e conexões são liberadas cedo?
- Há backpressure antes que milhares de tasks aguardem?
- TLS e credenciais são configurados fora do código?
- O shutdown foi testado sob carga?
- O repositório esconde o tipo concreto do pool do domínio?
- O time documentou por que escolheu Deadpool, bb8 ou SQLx?
Deadpool e bb8 como sinal de carreira
Em entrevistas de backend Rust, dizer “usei PostgreSQL” é menos forte que explicar:
- como calculou o pool por réplica;
- por que definiu timeout de aquisição;
- como evitou segurar conexão durante I/O externo;
- quais métricas detectam saturação;
- como o serviço se comporta em failover;
- por que escolheu o pool nativo ou um adapter.
Um projeto de portfólio pode combinar Axum, PostgreSQL, migrations, métricas e teste de esgotamento. Isso demonstra engenharia de produção e conversa diretamente com vagas Rust, empresas que usam Rust e a trilha de carreira em backend web.
Perguntas frequentes
Deadpool ou bb8: qual pool escolher em Rust?
Deadpool é uma escolha forte para adapters prontos e múltiplos tipos de recurso. bb8 é atraente quando a stack já oferece um ManageConnection ou quando você prefere seu contrato genérico. A compatibilidade com o driver deve decidir antes da preferência estética.
Preciso de Deadpool ou bb8 se já uso SQLx?
Normalmente não. Comece com sqlx::Pool. Use Deadpool ou bb8 para outro recurso ou integração que realmente precise deles, não como camada redundante.
Deadpool serve apenas para banco de dados?
Não. Ele gerencia recursos assíncronos reutilizáveis; PostgreSQL e Redis são adapters populares, mas managers customizados também são possíveis.
Como evitar que o pool derrube a API sob carga?
Combine limite correto, timeout de aquisição, backpressure, queries rápidas e métricas de espera. Não tente resolver saturação apenas aumentando conexões.
Qual é a diferença entre pool e semáforo?
Pool mantém recursos reutilizáveis. Semáforo limita concorrência. Os dois podem trabalhar juntos para proteger banco, memória e latência.
Como testar um pool de conexões em Rust?
Force esgotamento, timeout, conexão inválida, shutdown e concorrência entre réplicas. Teste o caminho de falha, não apenas uma query bem-sucedida.
Conclusão
A escolha entre Deadpool e bb8 é menos dramática do que a operação correta do pool. Para SQLx, fique no pool nativo. Para tokio-postgres, compare os adapters e escolha a API que o time consegue configurar, observar e testar. Para Redis e recursos variados, Deadpool costuma oferecer um caminho prático; para managers pequenos e integrações existentes, bb8 continua uma opção sólida.
O resultado maduro tem quatro propriedades: limite global pensado por réplica, timeout de aquisição, conexões mantidas pelo menor tempo possível e métricas que separam espera de execução. Continue pelo guia de SQLx em Rust, PostgreSQL com Rust, métricas Prometheus e resiliência com Tower/Axum. Um pool bem escolhido ajuda; um pool bem operado mantém o serviço vivo.