Back-end · Sênior

Resiliência: idempotência, retries e circuit breaker

Em sistemas distribuídos, falhas não são exceção — são rotina. A rede cai, um serviço fica lento, uma chamada estoura o timeout. Resiliência é projetar para que essas falhas inevitáveis não derrubem o sistema inteiro. Para um sênior, dominar idempotência, retries e circuit breaker é essencial — e tema frequente em entrevistas.

Falhas acontecem

A premissa que muda tudo: em escala, algo está sempre falhando. Um disco, uma instância, um link de rede. O objetivo não é evitar 100% das falhas (impossível), mas conter o impacto e se recuperar automaticamente. Resiliência é a diferença entre "um serviço lento" e "o sistema inteiro fora do ar".

Idempotência: a base do retry seguro

Uma operação é idempotente quando executá-la várias vezes produz o mesmo efeito de executá-la uma vez. Isso é crucial porque, quando uma requisição falha, você quer poder tentar de novo — mas só pode fazer isso com segurança se o retry não causar dano. Imagine cobrar um cliente: se a resposta se perdeu mas a cobrança aconteceu, um retry cego cobraria duas vezes.

A solução é a idempotency key: o cliente envia um identificador único da operação, e o servidor garante que processa cada chave apenas uma vez:

POST /cobrancas
Idempotency-Key: 7f3a-9c21

// servidor:
if jaProcessada("7f3a-9c21"):
    return resultadoSalvo("7f3a-9c21")   // nao cobra de novo
resultado = processarCobranca(...)
salvar("7f3a-9c21", resultado)
return resultado

Retry com backoff e jitter

Quando algo falha de forma transitória, retentar faz sentido. Mas retentar imediatamente e em loop pode piorar a situação — você martela um serviço já sobrecarregado. Por isso usa-se backoff exponencial: cada tentativa espera mais que a anterior (1s, 2s, 4s, 8s). E adiciona-se jitter (uma variação aleatória) para evitar que mil clientes retentem no mesmo instante exato, criando uma onda sincronizada.

espera = min(base * 2^tentativa, max) + random(0, jitter)
// tentativa 0: ~1s    tentativa 1: ~2s
// tentativa 2: ~4s    tentativa 3: ~8s

E sempre com timeout: nunca espere indefinidamente por uma resposta. Sem timeout, uma dependência lenta trava suas threads e o problema se propaga. Lembre também que retry só é seguro em operações idempotentes.

Circuit breaker

Se um serviço está claramente fora do ar, continuar tentando (mesmo com backoff) só desperdiça recursos e atrasa respostas. O circuit breaker funciona como um disjuntor elétrico: depois de muitas falhas seguidas, ele "abre" e passa a falhar rápido, sem nem tentar a chamada — dando tempo para o serviço se recuperar.

   sucesso                 muitas falhas
  +---------+   falhas    +---------+
  | FECHADO |  -------->  |  ABERTO |  (falha rapido,
  +---------+             +---------+   nem chama)
       ^                       |
       | sucesso               | apos timeout
       |                       v
       |  falha          +-------------+
       +---------------- | MEIO-ABERTO |  (testa 1 chamada)
                         +-------------+

Os três estados: fechado (normal, deixa passar), aberto (falha rápido após o limite de erros), e meio-aberto (após um tempo, deixa passar uma chamada de teste — se der certo, fecha; se falhar, reabre).

EstadoComportamentoTransição
FechadoChamadas passam normalmenteMuitas falhas → Aberto
AbertoFalha imediata, não chamaApós timeout → Meio-aberto
Meio-abertoTesta uma chamadaSucesso → Fechado / Falha → Aberto
Retry diz "tente de novo, com calma". Circuit breaker diz "pare de tentar, está claramente quebrado". Idempotência é o que torna o primeiro seguro. Os três trabalham juntos.

Isolamento com bulkhead

Por fim, o padrão bulkhead (numa frase): assim como um navio é dividido em compartimentos estanques para que um furo não afunde tudo, você isola recursos — pools de conexão ou threads separados por dependência — para que a falha de uma parte não consuma os recursos das outras.

Conclusão

Resiliência é projetar assumindo que tudo vai falhar. Torne operações idempotentes para permitir retries seguros, use backoff exponencial com jitter e timeouts para não agravar a carga, e proteja-se com circuit breakers e bulkheads para conter o estrago. Esse conjunto é o que mantém sistemas distribuídos de pé no mundo real.

In distributed systems, failures aren't the exception — they're routine. The network drops, a service gets slow, a call times out. Resilience is designing so that these inevitable failures don't take the whole system down. For a senior, mastering idempotency, retries and circuit breakers is essential — and a frequent interview topic.

Failures happen

The premise that changes everything: at scale, something is always failing. A disk, an instance, a network link. The goal isn't to avoid 100% of failures (impossible), but to contain the impact and recover automatically. Resilience is the difference between "a slow service" and "the entire system down".

Idempotency: the basis of safe retries

An operation is idempotent when running it several times has the same effect as running it once. This is crucial because, when a request fails, you want to be able to try again — but you can only do so safely if the retry doesn't cause damage. Imagine charging a customer: if the response got lost but the charge went through, a blind retry would charge twice.

The solution is the idempotency key: the client sends a unique identifier for the operation, and the server guarantees it processes each key only once:

POST /charges
Idempotency-Key: 7f3a-9c21

// server:
if alreadyProcessed("7f3a-9c21"):
    return savedResult("7f3a-9c21")   // doesn't charge again
result = processCharge(...)
save("7f3a-9c21", result)
return result

Retry with backoff and jitter

When something fails transiently, retrying makes sense. But retrying immediately and in a loop can make things worse — you hammer a service that's already overloaded. That's why exponential backoff is used: each attempt waits longer than the previous one (1s, 2s, 4s, 8s). And jitter (a random variation) is added to prevent a thousand clients from retrying at the exact same instant, creating a synchronized wave.

wait = min(base * 2^attempt, max) + random(0, jitter)
// attempt 0: ~1s    attempt 1: ~2s
// attempt 2: ~4s    attempt 3: ~8s

And always with a timeout: never wait indefinitely for a response. Without a timeout, a slow dependency ties up your threads and the problem spreads. Also remember that retrying is only safe on idempotent operations.

Circuit breaker

If a service is clearly down, continuing to try (even with backoff) only wastes resources and delays responses. The circuit breaker works like an electrical breaker: after many consecutive failures, it "opens" and starts failing fast, without even attempting the call — giving the service time to recover.

   success                 many failures
  +---------+  failures   +---------+
  | CLOSED  |  -------->  |  OPEN   |  (fails fast,
  +---------+             +---------+   doesn't call)
       ^                       |
       | success               | after timeout
       |                       v
       |  failure        +-------------+
       +---------------- |  HALF-OPEN  |  (tests 1 call)
                         +-------------+

The three states: closed (normal, lets calls through), open (fails fast after the error threshold), and half-open (after a while, lets one test call through — if it succeeds, it closes; if it fails, it reopens).

StateBehaviorTransition
ClosedCalls go through normallyMany failures → Open
OpenImmediate failure, no call madeAfter timeout → Half-open
Half-openTests a single callSuccess → Closed / Failure → Open
Retry says "try again, calmly". Circuit breaker says "stop trying, it's clearly broken". Idempotency is what makes the first one safe. The three work together.

Isolation with bulkhead

Finally, the bulkhead pattern (in one sentence): just as a ship is divided into watertight compartments so one hole doesn't sink the whole thing, you isolate resources — separate connection or thread pools per dependency — so that the failure of one part doesn't consume the resources of the others.

Conclusion

Resilience is designing on the assumption that everything will fail. Make operations idempotent to allow safe retries, use exponential backoff with jitter and timeouts so you don't worsen the load, and protect yourself with circuit breakers and bulkheads to contain the damage. That combination is what keeps distributed systems standing in the real world.

Leia também

Voltar à trilha