As anteparas do casco
O casco de um navio é dividido em compartimentos estanques. Um furo alaga o compartimento atingido e para ali: o navio aderna, perde velocidade, e continua flutuando. Sem as anteparas, o mesmo furo alagaria o casco inteiro. A antepara não impede o furo — ela decide até onde a água chega.
O recurso compartilhado é o canal da propagação
Um pool de 100 conexões parece folgado até uma dependência passar de 50ms para 30 segundos. Com 10 requisições por segundo tocando essa dependência, as 100 vagas se esgotam em dez segundos.
A partir daí, toda requisição nova entra na fila do pool — inclusive as que iriam para rotas saudáveis, que agora falham por um motivo que não tem nada a ver com elas.
É a mesma mecânica que o Timeout ataca, vista do outro lado. Timeout limita quanto tempo cada chamada pode segurar o recurso; Bulkhead limita quantas chamadas de um mesmo tipo podem segurá-lo ao mesmo tempo. Um teto de tempo sem teto de concorrência ainda permite que uma dependência ocupe tudo — só por menos tempo de cada vez.
Os três padrões dessa família se encaixam assim: Timeout garante que existe um fim; Bulkhead garante que o estrago fica contido enquanto esse fim não chega; Circuit Breaker garante que, depois de ficar claro que a coisa está ruim, nem se tenta. Nenhum substitui o outro.
Uma decisão que costuma passar despercebida: quando o compartimento enche, o que fazer? Enfileirar parece gentil e é a escolha errada por padrão — uma fila sem limite transforma indisponibilidade em latência crescente, e o usuário espera minutos por algo que vai falhar de qualquer jeito. Rejeitar rápido devolve o controle a quem chamou, que ainda pode escolher um caminho degradado.
Compartimentos
- Um teto de concorrência por dependência
- A lenta enche o compartimento dela e para ali
- As outras rotas seguem com a capacidade delas
- A degradação fica visível e localizada
O incidente continua existindo — mas fica do tamanho da dependência que o causou.
Como isso aparece na prática
O relatório que derrubou a API
Cenário
A mesma aplicação serve a API do produto e um punhado de relatórios administrativos que rodam consultas pesadas. Numa segunda-feira, três gestores pedem relatórios ao mesmo tempo e as consultas prendem todas as conexões do pool.
Como o padrão entra
Um compartimento de 5 conexões para relatórios e o restante para a API. Os relatórios passam a se enfileirar entre si e a API não percebe nada.
Trade-off: Relatórios simultâneos ficam mais lentos e alguns são recusados em horário de pico. Em compensação, deixam de ser capazes de derrubar a operação que paga a conta.
O tenant barulhento
Cenário
Num SaaS multiempresa, um cliente grande dispara uma importação em massa e consome sozinho quase toda a capacidade de processamento assíncrono. Os demais clientes veem suas filas paradas por horas.
Como o padrão entra
Compartimento por tenant, com teto proporcional ao plano contratado. A importação do cliente grande passa a usar apenas a fatia dele.
Trade-off: A importação grande demora mais do que demoraria com a máquina toda disponível, e sobra capacidade ociosa quando os outros tenants estão quietos. Isolamento sempre custa um pouco de eficiência.
Onde isto dá errado
Enfileirar sem limite quando o compartimento enche
Fila ilimitada não é gentileza: ela converte indisponibilidade em latência crescente, e o usuário espera minutos por uma requisição que vai falhar do mesmo jeito. O padrão certo é rejeitar rápido, e enfileirar só com teto e prazo.
Compartimentos pequenos demais
Dividir 100 conexões em dez compartimentos de dez faz cada rota estourar o teto sob carga normal, com o pool geral quase vazio. O objetivo não é reservar capacidade — é impedir que uma dependência tome tudo. A soma dos tetos pode e costuma passar do total.
Isolar a conexão e esquecer a thread
Separar pools de conexão não resolve se todas as chamadas continuam disputando o mesmo pool de threads ou o mesmo laço de eventos. O compartimento precisa cobrir o recurso que efetivamente satura primeiro.
Tetos definidos sem medição
Números escolhidos no chute costumam ser folgados demais para isolar ou apertados demais para servir. O teto se dimensiona a partir da concorrência real observada em pico, e precisa ser revisitado quando o tráfego muda de ordem de grandeza.
Achar que o anteparo evita a falha
O compartimento não deixa a dependência mais confiável — ele apenas contém o estrago. Quem espera que o anteparo melhore a disponibilidade da dependência vai concluir que ele não funcionou.
Compartimento com rejeição rápida
// ERRADO: um pool para tudo. A dependencia lenta consome as 100 vagas
// e derruba ate as rotas que nunca falam com ela.
const pool = criarPool({ max: 100 });
// CERTO: comparticoes. Cada dependencia tem um teto proprio,
// e o pior caso de uma nao alcanca as outras.
class Comparticao {
private emUso = 0;
constructor(private readonly limite: number) {}
async executar<T>(fn: () => Promise<T>): Promise<T> {
if (this.emUso >= this.limite) {
// Rejeitar rapido e melhor que enfileirar sem limite:
// fila infinita so adia a falha e piora a latencia de todos.
throw new Error("comparticao cheia");
}
this.emUso++;
try {
return await fn();
} finally {
this.emUso--;
}
}
}
const recomendacao = new Comparticao(10); // pode afogar sozinha
const pagamento = new Comparticao(40); // critico, tem folga
const catalogo = new Comparticao(50);import asyncio
class Comparticao:
"""Teto de concorrencia por dependencia, com rejeicao rapida."""
def __init__(self, limite: int):
self._sem = asyncio.Semaphore(limite)
async def executar(self, coro):
# locked() antes de acquire(): queremos REJEITAR, nao enfileirar.
if self._sem.locked():
raise RuntimeError("comparticao cheia")
async with self._sem:
return await coro
# Cada dependencia com seu teto. A soma pode passar do pool total —
# o objetivo nao e reservar capacidade, e impedir que UMA tome tudo.
recomendacao = Comparticao(10)
pagamento = Comparticao(40)Quando usar × quando evitar
Use quando
3- Um processo que atende rotas com criticidade diferente.
- Dependências com perfis de latência muito distintos.
- Ambientes multiempresa, onde um cliente pode afogar os demais.
Evite quando
3- Serviço com uma dependência só — não há o que isolar.
- Quando os tetos ficam tão apertados que desperdiçam capacidade.
- Como substituto de timeout ou de disjuntor: os três resolvem coisas diferentes.
Testar o que ficou
5 armadilhas de Bulkhead viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
Pools separados por dependênciaUm pool de conexões por serviço externo, em vez de um pool geral, é o padrão na forma mais direta.resources.limits do KubernetesTeto de CPU e memória por pod impede que um contêiner faminto derrube os vizinhos do mesmo nó.Os compartimentos de um navioO nome vem daí: a antepara isola a água ao compartimento furado e o casco continua flutuando.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Pool isolado: a falha de A não esgota B.
const httpPagamentos = agent({ maxSockets: 10 });O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Particionar recursos (pools, filas, threads) reduz o pico disponível para cada partição
- Dimensionar cada compartimento no tamanho certo é mais um parâmetro a ajustar e monitorar
Não vale se: há um consumidor só ou nenhum recurso compartilhado a proteger — sem contágio possível, compartimentar não isola nada.
Onde isto já custou caro
Incidentes reais que este conceito explica.
- Amazon Web ServicesUm comando digitado errado tira metade da internet do arNão havia compartimento: um único comando alcançou capacidade suficiente para derrubar o subsistema inteiro. Um teto por operação teria contido o estrago ao tamanho da intenção.
- CloudflareUma expressão regular consome 100% da CPU globalO firewall dividia CPU com o serviço de proxy. Sem teto para o módulo, ele consumiu a capacidade de que todo o resto dependia.
Onde isto aparece
Continue por aqui
- Circuit BreakerQuando uma dependência já está claramente fora do ar, continuar chamando não ajuda ninguém: gasta o seu prazo e atrapalha a recuperação dela. O disjuntor detecta o padrão de falha e passa a recusar na hora, até que valha a pena tentar de novo.
- TimeoutTodo pedido que sai da sua máquina precisa de um prazo máximo. Sem ele, a falha mais comum em produção não é o erro — é a espera infinita, que consome recursos até derrubar quem ainda estava saudável.
- Rate limitingCapacidade é finita, e demanda não pede licença. Limitar a taxa é escolher conscientemente quem é recusado quando a demanda passa do que se aguenta — em vez de deixar a sobrecarga escolher, degradando o serviço para todo mundo ao mesmo tempo.