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.
// 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)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
- Só faça retry em erros transitórios: timeouts, 5xx, conexão recusada. Nunca em 4xx (erro do cliente — não vai melhorar).
- Limite o número de tentativas: geralmente 2 a 3. Mais que isso é teimosia.
- Use exponential backoff: espere 100ms, depois 200ms, depois 400ms... assim a dependência tem tempo de respirar.
- 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.
- 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
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{}.
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).