Patrón Retry
Reintenta una operación que ha fallado con errores transitorios, usando estrategias configurables como delay fijo, backoff exponencial o integración con circuit breaker.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Patrón Retry
Resumen
El Patrón Retry es un patrón de resiliencia que maneja fallas transitorias reintentando una operación fallida. Las fallas transitorias son típicamente causadas por condiciones temporales como congestión de red, indisponibilidad temporal de servicios o timeouts. El patrón usa estrategias configurables — delay fijo, lineal o backoff exponencial — para evitar saturar el sistema objetivo.
Cuándo usarlo
Usa el Patrón Retry cuando:
- Los errores sean transitorios y probablemente se resuelvan al reintentar (timeouts de red, 503 Service Unavailable)
- La operación sea idempotente o pueda repetirse de forma segura
- Quieras mejorar la confiabilidad percibida sin intervención del usuario
- Necesites backoff configurable para evitar problemas de thundering herd
- Combínalo con Circuit Breaker para evitar reintentos cuando un servicio está claramente caído
Solución
Python
import time
from functools import wraps
def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempt = 1
current_delay = delay
while attempt <= max_attempts:
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
raise
print(f"Intento {attempt} falló: {e}. Reintentando en {current_delay}s...")
time.sleep(current_delay)
current_delay *= backoff
attempt += 1
return None
return wrapper
return decorator
@retry(max_attempts=3, delay=1, backoff=2, exceptions=(ConnectionError,))
def fetch_data(url: str):
import random
if random.random() < 0.7:
raise ConnectionError("Error de red")
return f"Data from {url}"
# Uso
try:
result = fetch_data("https://api.ejemplo.com")
print(result)
except ConnectionError:
print("Todos los intentos de reintento agotados")
JavaScript
function retry(fn, { maxAttempts = 3, delay = 1000, backoff = 2, exceptions = [Error] } = {}) {
return async function(...args) {
let attempt = 1;
let currentDelay = delay;
while (attempt <= maxAttempts) {
try {
return await fn(...args);
} catch (e) {
const isRetryable = exceptions.some(ex => e instanceof ex);
if (!isRetryable || attempt === maxAttempts) throw e;
console.log(`Intento ${attempt} falló: ${e.message}. Reintentando en ${currentDelay}ms...`);
await new Promise(r => setTimeout(r, currentDelay));
currentDelay *= backoff;
attempt++;
}
}
};
}
async function fetchData(url) {
if (Math.random() < 0.7) throw new Error("Error de red");
return `Data from ${url}`;
}
const retryFetch = retry(fetchData, { maxAttempts: 3, delay: 1000, backoff: 2 });
// Uso
retryFetch("https://api.ejemplo.com")
.then(console.log)
.catch(e => console.log("Todos los intentos agotados:", e.message));
Java
import java.util.function.Supplier;
public class Retry {
public static <T> T execute(Supplier<T> action, int maxAttempts, long delayMs, double backoff) {
int attempt = 1;
long currentDelay = delayMs;
while (attempt <= maxAttempts) {
try {
return action.get();
} catch (Exception e) {
if (attempt == maxAttempts) throw new RuntimeException("Todos los reintentos agotados", e);
System.out.println("Intento " + attempt + " falló. Reintentando en " + currentDelay + "ms...");
try {
Thread.sleep(currentDelay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrumpido durante delay de reintento", ie);
}
currentDelay = (long)(currentDelay * backoff);
attempt++;
}
}
throw new IllegalStateException("Inalcanzable");
}
}
// Uso
String result = Retry.execute(() -> {
if (Math.random() < 0.7) throw new RuntimeException("Error de red");
return "Data fetched";
}, 3, 1000, 2.0);
Explicación
El Patrón Retry tiene tres dimensiones configurables:
- Máximo de Intentos: Cuántas veces intentar antes de rendirse (incluyendo el intento inicial)
- Delay: El tiempo de espera inicial entre reintentos
- Estrategia de Backoff: Cómo crece el delay:
- Fijo: Mismo delay cada vez
- Lineal: El delay aumenta en una cantidad fija
- Exponencial: El delay se duplica (o multiplica) cada vez — mejor para la mayoría de escenarios
- Filtro de Excepciones: Qué excepciones se consideran reintentables
Variantes
| Variante | Descripción | Caso de uso |
|---|---|---|
| Delay Fijo | Espera constante entre reintentos | Carga predecible en el objetivo |
| Backoff Exponencial | El delay se duplica en cada reintento | Evita saturar servicios en recuperación |
| Jitter | Agrega aleatoriedad al backoff | Previene thundering herd tras recuperación |
| Circuit Breaker + Retry | Omite reintentos cuando el breaker está abierto | Previene reintentos desperdiciados |
Lo que funciona
- Haz las operaciones idempotentes antes de aplicar reintentos — los reintentos pueden causar efectos secundarios duplicados
- Usa backoff exponencial con jitter para sistemas distribuidos para evitar reintentos sincronizados
- Establece una duración máxima total (deadline) además de un máximo de intentos
- Registra cada intento de reintento con contexto para depuración
- Combina con Circuit Breaker — no reintentes cuando el objetivo está claramente caído
Errores comunes
- Reintentar operaciones no idempotentes sin mecanismos de deduplicación
- Usar backoff lineal o ausente, saturando un servicio en recuperación
- No establecer un límite máximo de reintentos, causando bucles infinitos
- Reintentar en errores no transitorios (ej. 400 Bad Request, fallas de autenticación)
- Ignorar tormentas de reintentos — muchos clientes reintentando simultáneamente tras una breve interrupción
Preguntas frecuentes
P: ¿Cuál es la diferencia entre Retry y Circuit Breaker? R: Retry maneja fallas transitorias individuales. Circuit Breaker previene fallas en cascada deteniendo peticiones a un servicio fallido. Funcionan mejor juntos: Retry maneja errores temporales, Circuit Breaker maneja interrupciones prolongadas.
P: ¿Debería reintentar errores 500 Internal Server Error? R: Depende. El 500 puede indicar un problema transitorio del servidor que vale la pena reintentar, pero 502/503/504 son más claramente transitorios. Nunca reintentes errores 4xx del cliente (400, 401, 403, 404) sin corregir la petición primero.
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Temas Avanzados
Escenario: Retry con Backoff para API Externa
// Retry pattern con backoff exponencial y jitter
async function retryWithBackoff<T>(
fn: () => Promise<T>,
opts: {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
retryOn?: (err: Error) => boolean;
}
): Promise<T> {
const { maxRetries, baseDelayMs, maxDelayMs, retryOn } = opts;
let lastError: Error;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err as Error;
if (attempt === maxRetries) break;
if (retryOn && !retryOn(err as Error)) break;
// Backoff exponencial con jitter
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * baseDelayMs;
const delay = Math.min(exponentialDelay + jitter, maxDelayMs);
await new Promise(r => setTimeout(r, delay));
}
}
throw lastError!;
}
// Uso: retry en llamada a Stripe
try {
const charge = await retryWithBackoff(
() => stripe.charges.create({ amount: 1000, currency: "usd" }),
{
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 10000,
retryOn: (err) => err.message.includes("rate_limit") || err.message.includes("timeout"),
}
);
} catch (err) {
console.error("Payment failed after retries:", err);
}
// Estrategias de backoff
| Estrategia | Formula | Ventajas | Desventajas |
|------------|---------|----------|-------------|
| Fijo | 1000ms | Simple | No adapta |
| Lineal | attempt * 1000 | Predecible | Lento |
| Exponencial | 2^attempt * 1000 | Rapido al inicio | Puede ser muy largo |
| Exponencial + jitter | 2^attempt * 1000 + random | Evita thundering herd | Menos predecible |
| Decorrelated | prev * 3 * random | Auto-adapta | Complejo |
// Cuando NO reintentar
| Error | Reintentar? | Razon |
|-------|-------------|--------|
| 400 Bad Request | No | Error del cliente, no se arregla |
| 401 Unauthorized | No | Credenciales invalidas |
| 403 Forbidden | No | Sin permisos |
| 404 Not Found | No | Recurso no existe |
| 429 Too Many Requests | Si | Rate limit temporal |
| 500 Internal Server | Si | Error temporal del server |
| 502 Bad Gateway | Si | Proxy temporal |
| 503 Service Unavailable | Si | Server sobrecargado |
| Timeout | Si | Red temporal |
| Connection refused | Si | Serverreiniciandose |
Lecciones:
- Retry con backoff exponencial + jitter es el estandar
- Jitter evita thundering herd: todos reintentan al mismo tiempo
- Solo reintentar errores temporales (5xx, timeout, rate limit)
- No reintentar errores de cliente (4xx): no se arreglan solos
- Max delay cap: evitar esperas de minutos
- Circuit breaker + retry: el retry opera dentro del circuito cerrado
### Como combino retry con circuit breaker?
El circuit breaker envuelve el retry. Si el circuito esta cerrado, ejecuta el retry. Si el circuito esta abierto, falla inmediatamente sin reintentar. El circuit breaker cuenta fallos por servicio, no por intento. El retry maneja fallos transitorios; el circuit breaker maneja fallos sostenidos. Configura el circuit breaker con failureThreshold > maxRetries para que no se abra por un solo request con reintentos. Recursos Relacionados
Circuit Breaker Pattern
Prevent cascading failures by stopping requests to failing services. An architectural pattern for resilient distributed systems.
PatternTimeout Pattern
Prevent operations from hanging indefinitely by enforcing a maximum execution time. A resilience pattern for predictable response times.
PatternBulkhead Pattern: Isolate Resources to Limit Blast Radius
How to isolate resources per service to limit blast radius. Covers thread pool isolation, connection pool partitioning, semaphore-based bulkheads, and resource quotas.
PatternCache-Aside Pattern
Load data into the cache on demand from the backing store. A caching pattern that gives the application full control over what and when to cache.
PatternSaga Pattern
Manage distributed transactions across multiple services by chaining local transactions with compensating actions for rollbacks. A microservices pattern.