Existe uma intuição errada e muito difundida: a falha perigosa é o serviço que cai.
Serviço caído devolve erro na hora. Você trata, mostra mensagem, segue. É o caso fácil.
O perigoso é o serviço lento. Ele não devolve erro: ele segura a sua requisição. E cada requisição segurada é uma conexão do pool, uma thread, um pedaço de memória, tudo parado esperando.
Com tráfego normal, uma dependência que passou de 200ms pra 8 segundos consome todos os seus recursos em minutos. A sua aplicação cai inteira, incluindo as partes que não usam aquela dependência.
Timeout: a configuração mais negligenciada
Quase toda biblioteca HTTP vem sem timeout por padrão, ou com um padrão de vários minutos. Isso significa que o seu sistema está disposto a esperar pra sempre por qualquer coisa.
const res = await fetch(url, {
signal: AbortSignal.timeout(3000),
});
Precisa existir timeout em três camadas, e elas frequentemente são confundidas:
Conexão, que é quanto tempo pra estabelecer o socket. Curto, por volta de um segundo.
Leitura, que é quanto tempo esperando resposta. Depende da operação.
E o total, que é o limite de tudo somado, incluindo retentativas. É o que protege de verdade.
Sem o total, três tentativas de 5 segundos com backoff viram 20 segundos de espera, e o usuário do outro lado já foi embora.
Escolher o número
Chutar valor redondo é como se cria o problema do gateway que responde em 6 segundos com timeout de 5.
O método: meça o percentil 99 da chamada em condição normal e use duas a três vezes isso.
Se o p99 é 400ms, timeout de 1 segundo. Se é 3 segundos, timeout de 8. Fora dessa faixa, ou você está cortando requisição legítima ou está esperando demais.
E timeout de operação síncrona, com usuário esperando, precisa ser menor que a paciência dele. Não adianta esperar 30 segundos por uma resposta que ninguém vai ver.
Circuit breaker: parar de tentar
Timeout limita o dano de cada chamada. Quando a dependência está fora de verdade, você continua gastando 3 segundos por requisição pra descobrir a mesma coisa toda vez.
O disjuntor conta as falhas numa janela. Passando do limite, abre: as chamadas seguintes falham imediatamente, sem tocar a rede. Depois de um tempo, deixa passar uma pra testar. Se der certo, fecha.
class CircuitBreaker {
private failures = 0;
private openedAt?: number;
constructor(
private readonly threshold = 5,
private readonly cooldownMs = 30_000,
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.openedAt && Date.now() - this.openedAt < this.cooldownMs) {
throw new CircuitOpenError(); // falha em microssegundos
}
try {
const result = await fn();
this.failures = 0;
this.openedAt = undefined;
return result;
} catch (err) {
if (++this.failures >= this.threshold) this.openedAt = Date.now();
throw err;
}
}
}
O ganho principal não é proteger o outro serviço. É proteger o seu: sem disjuntor, todas as suas threads ficam presas esperando timeout de algo que já se sabe que está morto.
Com ele, a funcionalidade que dependia daquilo degrada e o resto da aplicação continua de pé. Essa é a diferença entre “o relatório está indisponível” e “o site caiu”.
Anteparo: isolar por dependência
Se todas as chamadas externas compartilham o mesmo pool de conexões, uma dependência lenta consome o pool inteiro e derruba as chamadas pras outras.
A correção é dar a cada dependência a sua fatia:
const gatewayPool = new Semaphore(10);
const emailPool = new Semaphore(5);
const analyticsPool = new Semaphore(3);
Se o serviço de analytics travar, ele segura três slots. Os outros continuam. Isso tem nome, bulkhead, e vem da divisão em compartimentos estanques de navio: um compartimento alagado não afunda o barco.
O que fazer quando o disjuntor abre
Essa é a parte de produto, e é a que decide se a resiliência serve pra alguma coisa.
Para cada dependência, defina antes o que acontece quando ela não responde.
Serviço de recomendação fora: mostre os mais vendidos. Degradação invisível.
Serviço de e-mail fora: enfileire e mande depois. O usuário não precisa saber.
Gateway de pagamento fora: aí não tem plano B, e a mensagem precisa ser honesta e específica, com orientação de tentar mais tarde.
O que não pode acontecer é a página inteira quebrar porque um bloco lateral não carregou.
O teste que quase ninguém faz
Configuração de resiliência que nunca foi exercitada é ficção.
Em homologação, derrube uma dependência e observe. Depois faça pior: deixe ela lenta, respondendo em 30 segundos. O segundo cenário é o que quebra sistema, e é o que ninguém testa porque é mais difícil de simular que desligar.
Um proxy que injeta latência resolve, e uma tarde fazendo isso costuma revelar dois ou três lugares sem timeout que ninguém sabia que existiam.
A ordem que eu seguiria
Timeout em toda chamada externa, incluindo banco e cache. É a única não negociável, e é a que falta com mais frequência.
Depois statement_timeout no banco, que impede uma query ruim de segurar conexão.
Depois disjuntor nas dependências externas críticas.
E por último anteparo, que só importa quando existem várias dependências disputando o mesmo recurso.
Sobre a parte de retentativa, que anda junto com isso, escrevi em retry, backoff e dead letter queue.