Skip to content

Repetir com backoff exponencial snippet

A lógica de retry é fácil de escrever mal de quatro maneiras: sem limite de tentativas (loop infinito), sem jitter (todos os clientes repetem em uníssono e voltam a derrubar o servidor), repetindo erros 4xx permanentes que nunca podem resultar, e a deixar escapar a ligação da resposta falhada antes de dormir.

A lógica de retry é fácil de escrever mal de quatro maneiras: sem limite de tentativas (loop infinito), sem jitter (todos os clientes repetem em uníssono e voltam a derrubar o servidor), repetindo erros 4xx permanentes que nunca podem resultar, e a deixar escapar a ligação da resposta falhada antes de dormir. O padrão em toda a parte: limitar as tentativas, duplicar o atraso a cada ronda (base × 2^attempt), acrescentar jitter aleatório, repetir apenas 429/5xx e falhas de rede — e respeitar o Retry-After quando o servidor o envia. C, C++ e Zig ficam de fora: nenhum cliente HTTP na stdlib com que fazer retry.

Receita executável · 11 linguagens
HTTP & APIsretrybackoffjitterresiliencehttp

Every language

11 linguagens, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
async function fetchWithRetry(url, attempts = 5, baseMs = 200) {
  for (let attempt = 0; ; attempt++) {
    try {
      const res = await fetch(url);
      if (res.ok) return res;

      // 429 and 5xx may recover; other 4xx never will
      const retryable = res.status === 429 || res.status >= 500;
      if (!retryable || attempt === attempts - 1) {
        const err = new Error(`HTTP ${res.status}`);
        err.retryable = retryable;
        throw err;
      }
    } catch (err) {
      const last = attempt === attempts - 1;
      if (last || err.retryable === false) throw err;
    }
    const jitter = Math.random() * baseMs; // stops retries marching in lockstep
    await new Promise((r) => setTimeout(r, 2 ** attempt * baseMs + jitter));
  }
}

Network faults (fetch rejects with TypeError) have no retryable flag — undefined means retry. Honor Retry-After: parse res.headers.get('retry-after') and sleep that long when present.

TSTypeScript
interface RetryOptions {
  attempts?: number;
  baseMs?: number;
}

class HttpError extends Error {
  constructor(readonly status: number, readonly retryable: boolean) {
    super(`HTTP ${status}`);
  }
}

export async function fetchWithRetry(
  url: string,
  { attempts = 5, baseMs = 200 }: RetryOptions = {},
): Promise<Response> {
  for (let attempt = 0; ; attempt++) {
    try {
      const res = await fetch(url);
      if (res.ok) return res;
      throw new HttpError(
        res.status,
        res.status === 429 || res.status >= 500,
      );
    } catch (err) {
      if (attempt === attempts - 1 || (err instanceof HttpError && !err.retryable)) {
        throw err; // out of attempts, or a permanent 4xx
      }
      const jitter = Math.random() * baseMs;
      await new Promise((r) => setTimeout(r, 2 ** attempt * baseMs + jitter));
    }
  }
}

The typed HttpError carries the retryable verdict — callers can branch on it. Network faults (TypeError from fetch) fall into the retry path.

GoGo
func getWithRetry(ctx context.Context, url string, attempts int, base time.Duration) (*http.Response, error) {
	for attempt := 0; ; attempt++ {
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
		if err != nil {
			return nil, err
		}
		res, err := http.DefaultClient.Do(req)

		retryable := err != nil || res.StatusCode == http.StatusTooManyRequests || res.StatusCode >= 500
		if !retryable || attempt == attempts-1 {
			return res, err // success, permanent 4xx, or the final attempt
		}
		if res != nil {
			res.Body.Close() // leak the failed body or exhaust the pool
		}

		jitter := time.Duration(rand.Int64N(int64(base))) // math/rand/v2
		delay := base<<attempt + jitter
		select {
		case <-time.After(delay):
		case <-ctx.Done(): // cancellation beats retrying
			return nil, ctx.Err()
		}
	}
}

base<<attempt is the exponential in one shift. The select makes the retry loop cancellable — without ctx a killed service keeps retrying.

RsRust
use rand::Rng;
use std::time::Duration;

async fn get_with_retry(
    url: &str,
    attempts: u32,
    base: Duration,
) -> Result<String, reqwest::Error> {
    let mut attempt = 0;
    loop {
        attempt += 1;
        match reqwest::get(url).await.map(|r| r.error_for_status()) {
            Ok(Ok(res)) => return res.text().await,
            Ok(Err(err)) | Err(err) => {
                if attempt >= attempts
                    || err.status().is_some_and(|s| s.is_client_error())
                {
                    return Err(err); // out of attempts, or a permanent 4xx
                }
            }
        }
        let jitter = rand::rng().random_range(0..base.as_millis() as u64);
        tokio::time::sleep(base * 2u32.saturating_pow(attempt - 1) + Duration::from_millis(jitter)).await;
    }
}

error_for_status() folds 4xx/5xx into Err so one match handles both worlds; err.status() then separates permanent (client error) from retryable. rand 0.9 API.

PHPPHP
function getWithRetry(string $url, int $attempts = 5, int $baseMs = 200): string {
    for ($attempt = 0; ; $attempt++) {
        $body = @file_get_contents($url, false, stream_context_create([
            'http' => ['timeout' => 5, 'ignore_errors' => true],
        ]));

        if ($body !== false) {
            return $body; // production: also inspect the status line
        }
        if ($attempt === $attempts - 1) {
            throw new RuntimeException("GET $url failed after $attempts attempts");
        }
        // 2^attempt backoff + full-range jitter, in microseconds
        usleep((2 ** $attempt) * $baseMs * 1000 + random_int(0, $baseMs) * 1000);
    }
}

random_int is cryptographic — overkill for jitter but the always-safe choice in PHP; mt_rand is the lighter alternative.

PyPython
import random
import time
import httpx

def get_with_retry(url: str, attempts: int = 5, base: float = 0.2) -> httpx.Response:
    for attempt in range(attempts):
        try:
            response = httpx.get(url, timeout=5.0)
        except httpx.TransportError:
            if attempt == attempts - 1:
                raise
        else:
            retryable = response.status_code == 429 or response.status_code >= 500
            if not retryable or attempt == attempts - 1:
                return response
        time.sleep(base * 2 ** attempt + random.uniform(0, base))
    raise AssertionError('unreachable')

TransportError is the network-fault family (connect, read, write); raise_for_status() is deliberately NOT used — it would erase the status we need to classify.

C#C#
async Task<string> GetWithRetry(string url, int attempts = 5, double baseSec = 0.2)
{
    using var client = new HttpClient();
    for (var attempt = 0; ; attempt++)
    {
        try
        {
            var response = await client.GetAsync(url);
            var retryable = response.StatusCode == HttpStatusCode.TooManyRequests
                || (int)response.StatusCode >= 500;
            if (!retryable || attempt == attempts - 1)
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
        catch (HttpRequestException) when (attempt < attempts - 1)
        {
            // network fault — fall through to the delay
        }
        var delay = TimeSpan.FromSeconds(
            baseSec * Math.Pow(2, attempt) + Random.Shared.NextDouble() * baseSec);
        await Task.Delay(delay);
    }
}

Polly is the production resilience library: Policy.Handle<HttpRequestException>().WaitAndRetryAsync(...) — reach for it outside exercises.

JvJava
static String getWithRetry(URI uri, int attempts, Duration base)
        throws IOException, InterruptedException {
    HttpClient client = HttpClient.newHttpClient();
    for (int attempt = 0; ; attempt++) {
        HttpRequest request = HttpRequest.newBuilder(uri)
                .timeout(Duration.ofSeconds(5)).GET().build();
        try {
            HttpResponse<String> response =
                    client.send(request, HttpResponse.BodyHandlers.ofString());
            boolean retryable = response.statusCode() == 429
                    || response.statusCode() >= 500;
            if (!retryable || attempt == attempts - 1) {
                return response.body();
            }
        } catch (IOException e) { // network fault — retryable
            if (attempt == attempts - 1) throw e;
        }
        long jitter = ThreadLocalRandom.current().nextLong(base.toMillis());
        Thread.sleep(base.multipliedBy(1L << attempt).plusMillis(jitter).toMillis());
    }
}

1L << attempt is the exponential. Resilience4j is the production library (Retry.of with IntervalFunction.ofExponentialRandomBackoff).

SwSwift
func getWithRetry(url: URL, attempts: Int = 5, base: TimeInterval = 0.2) async throws -> Data {
    var attempt = 0
    while true {
        attempt += 1
        do {
            var request = URLRequest(url: url)
            request.timeoutInterval = 5
            let (data, response) = try await URLSession.shared.data(for: request)
            let code = (response as? HTTPURLResponse)?.statusCode ?? 0
            let retryable = code == 429 || code >= 500
            if !retryable || attempt == attempts { return data }
        } catch {
            if attempt == attempts { throw error } // network fault, last try
        }
        let jitter = Double.random(in: 0..<base)
        try await Task.sleep(for: .seconds(base * pow(2, Double(attempt - 1)) + jitter))
    }
}

Task.sleep(for:) needs iOS 16/macOS 13. Parse the Retry-After header when present — it overrides any computed delay.

KtKotlin
import kotlinx.coroutines.delay
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import kotlin.random.Random

suspend fun getWithRetry(url: String, attempts: Int = 5, baseMs: Long = 200): String {
    val client = HttpClient.newBuilder().build()
    var attempt = 0
    while (true) {
        attempt++
        val request = HttpRequest.newBuilder(URI.create(url))
            .timeout(Duration.ofSeconds(5)).GET().build()
        try {
            val response = client.send(request, HttpResponse.BodyHandlers.ofString())
            val retryable = response.statusCode() == 429 || response.statusCode() >= 500
            if (!retryable || attempt == attempts) return response.body()
        } catch (e: IOException) {
            if (attempt == attempts) throw e
        }
        delay((baseMs shl (attempt - 1)) + Random.nextLong(baseMs)) // jittered backoff
    }
}

delay (not Thread.sleep) keeps the coroutine off the thread while backing off — the whole point of suspend.

RbRuby
require 'net/http'
require 'uri'

def get_with_retry(url, attempts: 5, base: 0.2)
  uri = URI(url)
  attempts.times do |attempt|
    response = Net::HTTP.get_response(uri)
    retryable = response.code == '429' || response.code.to_i >= 500
    return response if !retryable || attempt == attempts - 1

    sleep(base * 2**attempt + rand * base) # full jitter
  end
end

Net::HTTP.get_response raises on network faults — wrap in begin/rescue retry when connection errors need the same treatment.