Retentativas, Backoff & Idempotência
Chamadas resilientes em LTE instável e modo metrô - quando tentar novamente, como fazer backoff e como as chaves de idempotência evitam cobranças duplicadas.
Busque em todas as páginas da documentação
Chamadas resilientes em LTE instável e modo metrô - quando tentar novamente, como fazer backoff e como as chaves de idempotência evitam cobranças duplicadas.
Cartão de receita de referência rápida - pronto para copiar e colar.
// src/api/retry.ts
export type RetryOptions = {
maxAttempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
shouldRetry?: (error: unknown, attempt: number) => boolean;
};
function jitter(ms: number): number {
return ms * (0.5 + Math.random() * 0.5);
}
export async function fetchWithRetry(
input: RequestInfo | URL,
init?: RequestInit,
opts: RetryOptions = {}
): Promise<Response> {
const {
maxAttempts = 4,
baseDelayMs = 500,
maxDelayMs = 8_000,
shouldRetry = defaultShouldRetry,
} = opts;
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const res = await fetch(input, init);
if (res.status === 429) {
const retryAfter = res.headers.get("Retry-After");
const waitSec = retryAfter ? Number(retryAfter) : 2 ** attempt;
await sleep(jitter(waitSec * 1000));
continue;
}
if (res.ok || !shouldRetry(res, attempt)) return res;
lastError = new Error(`HTTP ${res.status}`);
} catch (error) {
lastError = error;
if (!shouldRetry(error, attempt)) throw error;
}
if (attempt < maxAttempts) {
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
await sleep(jitter(delay));
}
}
throw lastError;
}
function defaultShouldRetry(error: unknown, attempt: number): boolean {
if (attempt >= 4) return false;
if (error instanceof Response) {
const status = error.status;
return status >= 500 || status === 408 || status === 429;
}
if (error instanceof Error) {
if (error.name === "AbortError") return false;
return error instanceof TypeError; // blip de rede
}
return false;
}
function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}Quando usar isto:
import NetInfo from "@react-native-community/netinfo";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";
import { fetchWithRetry } from "../api/retry";
const API = process.env.EXPO_PUBLIC_API_URL ?? "https://api.example.com";
function idempotencyKey(): string {
return `idem-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
async function createOrder(items: { sku: string; qty: number }[], key: string) {
const res = await fetchWithRetry(
`${API}/orders`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({ items }),
},
{
maxAttempts: 3,
shouldRetry: (err, attempt) => {
if (err instanceof Response) return err.status >= 500 && attempt < 3;
return err instanceof TypeError && attempt < 3;
},
}
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export function useCreateOrder() {
const queryClient = useQueryClient();
const waitForOnline = useCallback(async () => {
const state = await NetInfo.fetch();
if (state.isConnected && state.isInternetReachable !== false) return;
await new Promise<void>((resolve) => {
const unsub = NetInfo.addEventListener((s) => {
if (s.isConnected && s.isInternetReachable !== false) {
unsub();
resolve();
}
});
});
}, []);
return useMutation({
mutationFn: async (items: { sku: string; qty: number }[]) => {
await waitForOnline();
const key = idempotencyKey();
return createOrder(items, key);
},
retry: false, // retentativas tratadas em fetchWithRetry com chave de idempotência
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["orders"] }),
});
}O que isto demonstra:
Idempotency-Key permite que o servidor retorne o mesmo pedido se o cliente tentar novamente um POST após um timeout.Retry-After quando presente.retry: false em useMutation quando a retentativa na camada de transporte já está em execução com chaves - evita chamadas duplicadas da função de mutação com novas chaves.waitForOnline controla o checkout iniciado pelo usuário - opcional; filas em segundo plano usam padrões diferentes em UX de Falha de Rede.| Método / caso | Tentar novamente? | Condição |
|---|---|---|
| GET, HEAD | ✅ Sim | Tentativas limitadas + backoff |
| PUT com id estável | ✅ Geralmente | Servidor atualiza por id do recurso |
| DELETE | ✅ Geralmente | Segunda exclusão deve retornar 404 com segurança |
| POST de pagamento | ⚠️ Apenas com chave de idempotência | Servidor armazena a chave → mesma resposta |
| POST sem chave | ❌ Não | Risco de linhas/cobranças duplicadas |
| 401 / 403 | ❌ Não | Atualize a autenticação primeiro |
| 400 validação | ❌ Não | Payload está incorreto - corrija o cliente |
| 409 conflito | ❌ Não | Mescle ou mostre na UI |
type IdempotentRequest = {
idempotencyKey: string; // UUID gerado pelo cliente
operation: "createOrder" | "capturePayment";
payload: unknown;
};Idempotency-Key - alinhe o nome do cabeçalho com o provedorexport const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: (failureCount, error) => {
if (failureCount >= 3) return false;
if (error instanceof Error && error.message.startsWith("HTTP 4")) return false;
return true;
},
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000),
},
mutations: {
retry: 0,
},
},
});retry: 0 por padrão - idempotência explícita na camada de transporte em vez disso| Sintoma | Causa provável | Estratégia de retentativa |
|---|---|---|
TypeError: Network request failed | Handoff, túnel, avião | Backoff; pause se offline |
| Congela e depois expira | Sinal fraco | AbortController + retentativa |
| HTTP 502/503 | Recuperação do gateway | Retentativa com jitter |
| Intermitente 200 + corpo vazio | Falha na CDN | Guarda de análise; retentativa única |
| Sucesso no servidor, timeout no cliente | ACK lento | Chave de idempotência no POST |
for (const item of queue) {
try {
await fetchWithRetry(url, { headers: { "Idempotency-Key": item.key }, ... });
dequeue(item);
} catch {
incrementAttempts(item);
if (item.attempts > 5) surfaceToUser(item);
}
}| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
fetchWithRetry de transporte | Cliente fetch personalizado | Já está usando axios-retry com as mesmas regras |
TanStack Query retry | Consultas GET | Mutações não idempotentes |
| Replay do TaskManager em segundo plano | Uploads grandes horas depois | Feedback imediato do usuário necessário |
| Long-polling do servidor | Orçamento de retentativa do cliente esgotado | REST normal com timeouts curtos |
3–4 para leituras com backoff exponencial. 0–1 para mutações, a menos que as chaves de idempotência sejam garantidas.
Aleatorização do atraso (por exemplo, 500–1000 ms em vez de exatamente 750 ms) para que milhares de dispositivos não tentem novamente no mesmo milissegundo quando uma torre volta a funcionar.
Não - o recurso está faltando ou o URL está incorreto. Tentar novamente desperdiça bateria e obscurece bugs.
Chaves de idempotência dizem ao servidor para desduplicar dentro de um TTL. IDs do cliente se tornam o ID do recurso no payload - use ambos para criações offline quando possível.
Pode substituir, se configurado com as mesmas regras de idempotência e status. Utilitários agnósticos de transporte mantêm fetch e axios consistentes.
Versões do Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026