Definition
Cuando un servicio que estás llamando falla, tu primer instinto podría ser reintentar inmediatamente - y luego reintentar de nuevo, y de nuevo. Pero imagina miles de clientes haciendo esto simultáneamente: un servidor que apenas estaba empezando a recuperarse de repente recibe un pico masivo de tráfico de reintentos, empujándolo de vuelta al fallo. Por esto existe el backoff: es una estrategia de esperar progresivamente más tiempo entre intentos de reintento, dando tiempo a los sistemas para recuperarse.
Los algoritmos de backoff calculan el retraso entre intentos de reintento, típicamente aumentando el retraso con cada fallo. El enfoque más común es exponential backoff: esperar 1 segundo, luego 2 segundos, luego 4, luego 8, y así sucesivamente. Pero el exponential backoff puro tiene un problema - si muchos clientes empiezan al mismo tiempo y usan el mismo algoritmo, todos reintentarán al mismo tiempo, creando olas coordinadas de tráfico llamadas “thundering herd” (estampida). La solución es jitter: añadir aleatoriedad al retraso para que los clientes naturalmente dispersen sus reintentos.
La combinación de exponential backoff con jitter es el estándar de oro para lógica de reintento en sistemas distribuidos. Balancea persistencia (seguir intentando) con paciencia (esperar más cada vez) y justicia (no todos reintentan a la vez). AWS, Google Cloud, y virtualmente cada proveedor importante de API recomienda este enfoque en sus SDKs y documentación.
Example
Comportamiento por Defecto del SDK de AWS: Cuando llamas a servicios AWS y te limitan, el SDK automáticamente aplica exponential backoff con jitter. Primer reintento después de ~100ms, luego ~200ms, luego ~400ms, con variación aleatoria. Por esto los SDKs de AWS “simplemente funcionan” incluso cuando los servicios están bajo carga - el backoff está integrado.
Ethernet CSMA/CD: El algoritmo de exponential backoff original viene de redes Ethernet. Cuando dos dispositivos transmiten simultáneamente y colisionan, cada uno elige un retraso aleatorio (de una ventana exponencialmente creciente) antes de reintentar. Este algoritmo fundamental de los años 70 todavía se usa en software hoy.
Recuperación de Rate Limiting de API: Cuando la API de Twitter devuelve un 429 (rate limited), los clientes deberían hacer backoff exponencialmente. Reintentar inmediatamente solo quema más capacidad de rate limit. Hacer backoff deja que tu cuota se recupere y permite que otros clientes obtengan su parte justa.
Colas de Reintento de Entrega de Email: Los servidores SMTP implementan backoff cuando la entrega falla. Primer reintento después de 1 minuto, luego 5 minutos, luego 15, luego 30, luego cada hora, luego cada pocas horas. Esto da tiempo a los servidores receptores para volver online sin abrumarlos con intentos de reintento.
Backoff de Reinicio de Pod en Kubernetes: Cuando un contenedor crashea repetidamente, Kubernetes aplica exponential backoff a los reinicios: 10s, 20s, 40s, hasta 5 minutos. Esto previene que un crash loop consuma todos los recursos del cluster mientras todavía intenta recuperarse.
Analogy
El Toque Educado: Tocas la puerta de alguien y no contestan. ¿Tocas de nuevo inmediatamente? No - esperas unos segundos. ¿Todavía no contestan? Esperas un poco más. Cada vez, les das más tiempo para llegar a la puerta. Eventualmente, si claramente no están en casa, paras e intentas más tarde. Eso es exponential backoff.
El Restaurante Lleno: Un restaurante popular está lleno. Podrías pararte en el podio del anfitrión preguntando “¿hay mesa ya?” cada 30 segundos, molestando a todos. O podrías volver en 5 minutos, luego 10 minutos, luego 15 minutos. El segundo enfoque es backoff - eres persistente pero no molesto.
El Sistema de Semáforos: Cuando hay un embotellamiento, no aceleras a fondo cada vez que el tráfico se detiene. Esperas un momento, avanzas un poco, esperas más, avanzas de nuevo. Todos haciendo esto naturalmente crea flujo. Todos acelerando a fondo simultáneamente crea bloqueo. Jitter funciona de la misma manera - aleatorizar cuándo “avanzas” previene que todos se muevan a la vez.
La Señal de Ocupado y Callback: En la era pre-buzón de voz, cuando obtenías señal de ocupado, no remarcabas inmediatamente. Esperabas un minuto e intentabas de nuevo. Quizás esperabas un poco más la siguiente vez. Cada fallo significaba una espera más larga, dando tiempo a la otra persona para terminar su llamada.
Code Example
// Varios algoritmos de backoff con implementaciones
// Exponential backoff simple
function exponentialBackoff(attempt: number, baseMs: number = 100): number {
return baseMs * Math.pow(2, attempt);
// Intento 0: 100ms, Intento 1: 200ms, Intento 2: 400ms, etc.
}
// Exponential backoff con jitter (recomendado)
function exponentialBackoffWithJitter(
attempt: number,
baseMs: number = 100,
maxMs: number = 30000
): number {
const exponential = Math.min(baseMs * Math.pow(2, attempt), maxMs);
// Jitter completo: valor aleatorio entre 0 y retraso calculado
return Math.random() * exponential;
}
// "Equal jitter" estilo AWS - jitter descorrelacionado
function equalJitterBackoff(
attempt: number,
baseMs: number = 100,
maxMs: number = 30000
): number {
const exponential = Math.min(baseMs * Math.pow(2, attempt), maxMs);
// Mitad exponencial + mitad aleatorio
return exponential / 2 + Math.random() * (exponential / 2);
}
// Jitter descorrelacionado de AWS (recomendado por AWS)
function decorrelatedJitter(
previousDelayMs: number,
baseMs: number = 100,
maxMs: number = 30000
): number {
return Math.min(maxMs, Math.random() * (previousDelayMs * 3 - baseMs) + baseMs);
}
// Implementación completa de reintento con backoff
interface BackoffConfig {
baseDelayMs: number;
maxDelayMs: number;
maxRetries: number;
jitterType: 'none' | 'full' | 'equal' | 'decorrelated';
}
async function retryWithBackoff<T>(
operation: () => Promise<T>,
config: BackoffConfig = {
baseDelayMs: 100,
maxDelayMs: 30000,
maxRetries: 5,
jitterType: 'full'
}
): Promise<T> {
let lastError: Error;
let previousDelay = config.baseDelayMs;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === config.maxRetries) break;
// Calcular retraso basado en tipo de jitter
let delay: number;
switch (config.jitterType) {
case 'none':
delay = exponentialBackoff(attempt, config.baseDelayMs);
break;
case 'full':
delay = exponentialBackoffWithJitter(
attempt,
config.baseDelayMs,
config.maxDelayMs
);
break;
case 'equal':
delay = equalJitterBackoff(
attempt,
config.baseDelayMs,
config.maxDelayMs
);
break;
case 'decorrelated':
delay = decorrelatedJitter(
previousDelay,
config.baseDelayMs,
config.maxDelayMs
);
previousDelay = delay;
break;
}
console.log(`Intento ${attempt + 1} fallido, backoff de ${delay}ms`);
await sleep(delay);
}
}
throw lastError!;
}
// Backoff lineal (no recomendado pero a veces necesario)
function linearBackoff(attempt: number, baseMs: number = 1000): number {
return baseMs * (attempt + 1);
// Intento 0: 1000ms, Intento 1: 2000ms, Intento 2: 3000ms, etc.
}
// Backoff de Fibonacci (alternativa interesante)
function fibonacciBackoff(attempt: number, baseMs: number = 100): number {
const fib = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55];
return baseMs * (fib[Math.min(attempt, fib.length - 1)]);
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Diagram
flowchart TD
subgraph NoBackoff["Sin Backoff"]
A1[Fallo] --> A2[Reintento Inmediato]
A2 --> A3[Fallo]
A3 --> A4[Reintento Inmediato]
A4 --> A5[¡Servidor Sobrecargado!]
end
subgraph WithBackoff["Con Exponential Backoff + Jitter"]
B1[Fallo] --> B2[Esperar 100ms + jitter]
B2 --> B3[Reintento - Fallo]
B3 --> B4[Esperar 200ms + jitter]
B4 --> B5[Reintento - Fallo]
B5 --> B6[Esperar 400ms + jitter]
B6 --> B7[Reintento - ¡Éxito!]
end
subgraph ThunderingHerd["Prevención de Thundering Herd"]
C1[1000 Clientes Fallan]
C2[Con Jitter: Dispersos en 10s]
C3[Sin Jitter: Todos reintentan a la vez]
C1 --> C2
C1 --> C3
C2 --> C4[Recuperación Gradual]
C3 --> C5[Sobrecarga Inmediata]
end
style A5 fill:#f87171
style B7 fill:#86efac
style C4 fill:#86efac
style C5 fill:#f87171
Security Notes
Requisitos Principales:
- ADVERTENCIA - Los patrones de backoff predecibles pueden ser explotados.
- Si un atacante conoce tu timing exacto de reintento, puede cronometrar sus ataques para coincidir con tus olas de reintento, maximizando el daño a sistemas en recuperación.
- Siempre usa jitter para añadir imprevisibilidad a tu backoff.
- Esto previene que los atacantes predigan cuándo ocurrirán los reintentos y hace más difícil coordinar ataques con tus patrones de reintento.
- Ten cuidado con backoff en escenarios de autenticación.
Mejores Prácticas:
- Exponential backoff en fallos de login puede usarse para denegación de servicio - un atacante puede bloquear usuarios legítimos fallando intencionalmente sus intentos de login, disparando períodos largos de backoff.
- Considera implementar backoff por cliente en lugar de backoff global.
- Esto previene que el comportamiento de un cliente abusivo afecte el timing de backoff para todos los demás clientes.
- Monitorea clientes que no respetan el backoff.
- Si un cliente ignora respuestas 429 y headers Retry-After, podrían ser maliciosos o mal configurados y deberían ser limitados o bloqueados..
Best Practices
- Siempre usa jitter - Jitter completo o descorrelacionado previene thundering herd; nunca uses exponential backoff puro en producción
- Establece un límite de retraso máximo - Los retrasos no deberían crecer para siempre; 30-60 segundos es usualmente suficiente
- Respeta headers Retry-After - Cuando un servidor te dice cuándo reintentar, usa eso en lugar de tu propio cálculo
- Registra eventos de backoff - Rastrea cuándo ocurre backoff para identificar dependencias problemáticas
- Haz el backoff configurable - Diferentes operaciones pueden necesitar diferentes parámetros de backoff
- Comienza con retrasos base pequeños - 100-200ms es usualmente un buen punto de partida
- Usa backoff con circuit breakers - Los circuit breakers previenen reintentos completamente cuando un servicio está caído
- Considera prioridad de peticiones - Peticiones de alta prioridad podrían usar backoff menos agresivo
- Prueba comportamiento de backoff - Simula fallos para verificar que el backoff funciona como se espera
- Documenta tu estrategia de backoff - Los futuros mantenedores necesitan entender la lógica
Common Mistakes
Sin jitter: Exponential backoff puro causa olas de reintento sincronizadas, potencialmente peor que no tener backoff.
Retraso base muy corto: 1ms de retraso base con exponential backoff todavía produce ráfagas de reintentos antes de que se acumulen tiempos de espera significativos.
Sin límite máximo: Crecimiento exponencial sin límite lleva a retrasos absurdos (2^20 base = millones de ms = horas).
Ignorar Retry-After: Cuando un servidor proporciona timing explícito de reintento, tu algoritmo de backoff está sobrescribiendo conocimiento experto.
Backoff por operación en lugar de por endpoint: Si un endpoint está fallando, hacer backoff en todos los endpoints penaliza servicios saludables.
Resetear backoff muy agresivamente: Un éxito no debería resetear al retraso base si el servicio todavía está luchando. Considera decay en su lugar.
No hacer backoff en 429: Las respuestas de rate limit siempre significan “más despacio” - reintentar inmediatamente empeora el problema.