Retry Pattern do jeito certo

Por que sua implementação (provavelmente) está errada e como o "Jitter" salva seu sistema

Em arquitetura de sistemas distribuídos, uma regra é clara: a rede vai falhar. APIs vão ficar lentas. Serviços vão cair. Isso não é uma possibilidade, é uma certeza. São as falhas transitórias.
Qual a primeira solução que vem à mente para lidar com isso? Um Retry Pattern. Se a chamada falhar, tente de novo. Simples, certo?
Quase. Na verdade, uma implementação ingênua de retry não apenas não resolve o problema, como pode ser a causa de uma falha em cascata que derruba seu sistema inteiro.

O Problema: O "Estouro da Manada" (Thundering Herd)

Vamos imaginar um cenário:
O Serviço-B (ex: um serviço de pagamento) fica lento. O Serviço-A (ex: um e-commerce), que consome o Serviço-B, tem 100 instâncias rodando.
Um cliente tenta pagar. A chamada do Serviço-A para o Serviço-B dá timeout.
O Serviço-A tem um retry simples: "Falhou? Tente de novo imediatamente."
Todas as 100 instâncias do Serviço-A começam a bombardear o Serviço-B com retries imediatos para milhares de requisições.
Resultado: Você acabou de criar um ataque de negação de serviço (DDoS) contra seu próprio sistema. O Serviço-B, que estava apenas lento, agora está morto. Isso é o "Estouro da Manada".

A Solução Intermediária: Exponential Backoff

Ok, então retries imediatos são ruins. A próxima solução lógica é o Exponential Backoff: esperar antes de tentar de novo, e aumentar essa espera a cada falha.
Falha 1: Espere 1 segundo.
Falha 2: Espere 2 segundos.
Falha 3: Espere 4 segundos.
Falha 4: Espere 8 segundos.
Isso é muito melhor. Damos tempo para o Serviço-B respirar e se recuperar.
Mas ainda temos um problema, mais sutil. Se as 100 instâncias do Serviço-A falharem ao mesmo tempo, elas vão esperar 1 segundo e tentar de novo... ao mesmo tempo. Depois, vão esperar 2 segundos e tentar de novo... ao mesmo tempo.
Você não eliminou o estouro da manada; você apenas o agendou. Você criou picos de carga sincronizados que ainda podem sobrecarregar o Serviço-B.

A Solução Sênior: Exponential Backoff + Jitter

Aqui está o pulo do gato que separa uma arquitetura robusta de uma frágil: Jitter (tremulação, ou aleatoriedade).
A ideia é simples: adicione um pequeno fator aleatório ao seu tempo de espera. Em vez de todas as instâncias esperarem exatamente 4 segundos, elas esperarão entre 3 e 5 segundos (por exemplo).
O Jitter "espalha" as novas tentativas no tempo, quebrando aquele pico de carga sincronizado e transformando-o em uma carga distribuída e muito mais gerenciável para o serviço dependente.

Um exemplo de implementação (Pseudocódigo/C#):

// Fórmula básica de Exponential Backoff
int baseDelayMs = 1000; // 1 segundo
int maxRetries = 5;
var random = new Random();

for (int attempt = 0; attempt < maxRetries; attempt++)
{
    try
    {
        // 1. Tenta a operação
        await CallMyService();
        return; // Sucesso!
    }
    catch (TransientException ex)
    {
        // 2. Falhou? Calcula o backoff
        int backoffMs = (int)Math.Pow(2, attempt) * baseDelayMs;
        
        // 3. Adiciona Jitter
        // (ex: adiciona um valor aleatório entre 0ms e 500ms)
        int jitterMs = random.Next(0, 500); 
        
        int totalDelay = backoffMs + jitterMs;
        
        await Task.Delay(totalDelay);
    }
}

// Se chegou aqui, todas as tentativas falharam.
throw new Exception("Serviço indisponível após N tentativas.");

Conclusão

Não basta apenas re-tentar. Em sistemas distribuídos, é preciso re-tentar de forma inteligente.
Uma estratégia de retry sem Exponential Backoff é perigosa. Uma estratégia de Exponential Backoff sem Jitter é incompleta. Ao combinar os três, você garante que seus mecanismos de resiliência não se tornem a causa da sua próxima outage.