M06·04Padrões de Resiliência

CAPÍTULO 04

Padrões de Resiliência

Timeout, retry com backoff exponencial e circuit breaker — os três padrões que separam backends que sobrevivem dos que viram postmortem.

Por Thiago Souza12 min de leituraAtualizado em 2026-05

Resiliência é o que separa um backend amador de um profissional. A premissa é cruel mas verdadeira: tudo vai falhar. Rede, banco, dependência externa, disco, DNS. Sua app não pode cair junto.

Vamos ver os três padrões mais essenciais.

Timeout

O mais simples e o mais ignorado. Toda chamada externa precisa de timeout. Toda. Sem exceção.

Sem timeout, uma dependência travada trava sua app inteira. As goroutines/threads ficam presas esperando, novas requisições enfileiram, memória sobe, e em alguns minutos seu serviço — que estava ótimo — caiu junto com o vizinho.

go
// ERRADO: vai esperar até a Terra parar de girar
resp, err := http.Get("https://api.externa.com/data")
 
// CERTO: timeout explícito
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get("https://api.externa.com/data")
 
// AINDA MELHOR: context com timeout (cancelável e propagável)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
 
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.externa.com/data", nil)
resp, err := http.DefaultClient.Do(req)
Regra do polegar: o timeout do cliente deve ser menor que o timeout do servidor que o chama. Senão, o servidor desiste antes do cliente, e você não sabe se a operação foi feita ou não.

Retry

Quando uma chamada falha, talvez tenha sido uma falha transitória (glitch de rede, deploy do outro serviço, etc.). Tentar de novo pode resolver. Mas com cuidado, ou você cria os famosos retry storms.

As regras de ouro do retry

  1. Só faça retry em erros transitórios: timeouts, 5xx, conexão recusada. Nunca em 4xx (erro do cliente — não vai melhorar).
  2. Limite o número de tentativas: geralmente 2 a 3. Mais que isso é teimosia.
  3. Use exponential backoff: espere 100ms, depois 200ms, depois 400ms... assim a dependência tem tempo de respirar.
  4. Adicione jitter (aleatoriedade): se 1.000 clientes falham ao mesmo tempo e todos esperam exatamente 200ms, eles batem juntos de novo. Adicione um random pequeno.
  5. Idempotência é sagrada: retry só é seguro se a operação for idempotente. Fazer retry de um POST que cobra cartão de crédito sem cuidado = cliente cobrado duas vezes.

Exemplo conceitual em Go

go
func callWithRetry(ctx context.Context) error {
    const maxAttempts = 3
    var lastErr error
 
    for attempt := 0; attempt < maxAttempts; attempt++ {
        err := doCall(ctx)
        if err == nil {
            return nil // sucesso
        }
        if !isRetryable(err) {
            return err // erro permanente, não insiste
        }
        lastErr = err
 
        // exponential backoff com jitter
        base := time.Duration(1<<attempt) * 100 * time.Millisecond // 100, 200, 400ms
        jitter := time.Duration(rand.Int63n(int64(base) / 2))
        wait := base + jitter
 
        select {
        case <-time.After(wait):
            // continua
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    return fmt.Errorf("falha após %d tentativas: %w", maxAttempts, lastErr)
}

Circuit Breaker

Imagina o disjuntor da sua casa. Se há um curto-circuito, ele desliga antes que a casa pegue fogo. O Circuit Breaker faz isso para chamadas a serviços externos.

A ideia: se um serviço está falhando muito, pare de tentar por um tempo. Você protege:

  • A si mesmo: não fica gastando recursos (threads, conexões, memória) em chamadas que vão falhar.
  • O serviço quebrado: dando tempo pra ele se recuperar (sem ser bombardeado de requisições).

Os três estados

stateDiagram-v2
  state "CLOSED" as closed
  state "OPEN" as open
  state "HALF-OPEN" as halfopen
  [*] --> closed
  closed --> open : muitas falhas
  open --> halfopen : timeout
  halfopen --> closed : sucesso
  halfopen --> open : testa 1 req — falhou
  • CLOSED (fechado): estado normal. Tudo passa.
  • OPEN (aberto): atingiu o limiar de falhas. Todas as chamadas falham imediatamente, sem nem tentar (fail-fast). Você pode retornar um valor padrão, cache, ou erro pro cliente.
  • HALF-OPEN (meio-aberto): após um tempo (ex: 30s), deixa uma requisição passar pra testar. Se passar, volta a CLOSED. Se falhar, volta a OPEN por mais um tempo.

Em Go com sony/gobreaker

A v2 da biblioteca usa generics — o resultado é tipado, sem interface{}.

go
import "github.com/sony/gobreaker/v2"
 
// o tipo genérico define o retorno de Execute
cb := gobreaker.NewCircuitBreaker[*PaymentResponse](gobreaker.Settings{
    Name:        "payment-api",
    MaxRequests: 1,                // requisições em half-open
    Interval:    60 * time.Second, // janela pra contar falhas
    Timeout:     30 * time.Second, // tempo em OPEN antes de tentar half-open
    ReadyToTrip: func(counts gobreaker.Counts) bool {
        // abre se mais de 50% falhou e tem pelo menos 5 requisições
        failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
        return counts.Requests >= 5 && failureRatio >= 0.5
    },
})
 
// resp já é *PaymentResponse — sem type assertion
resp, err := cb.Execute(func() (*PaymentResponse, error) {
    return callPaymentAPI()
})

A combinação clássica

Em produção, esses três padrões andam juntos:

[Sua App] → Timeout → Retry (com backoff) → Circuit Breaker → [Serviço externo]

Plus um quarto opcional: Bulkhead (isola pools de recursos) e Fallback (resposta padrão quando tudo falha — ex: cache antigo).