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.
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
Notas de Producción
- Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
- Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.
Puntos Clave
- Aplica patrón retry cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
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.
## Troubleshooting
- **Pattern does not fit the problem**: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- **Too many abstractions**: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- **Tight coupling after refactoring**: check that interfaces are stable and dependencies point inward.
- **Tests break when the design changes**: favor stable contracts over internal structure.
- **Performance regression from indirection**: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
## Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón. Preguntas frecuentes
¿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.
Recursos Relacionados
Patrón Circuit Breaker
Previene fallos en cascada deteniendo solicitudes a servicios que están fallando. Un patrón arquitectural para sistemas distribuidos resilientes.
PatternPatrón Timeout
Previene que las operaciones se cuelguen indefinidamente imponiendo un tiempo máximo de ejecución. Un patrón de resiliencia para tiempos de respuesta predecibles.
PatternPatrón Bulkhead: Isolá Resources para Limitar Blast Radius
Cómo isolatar resources per service para limitar blast radius. Cubre thread pool isolation, connection pool partitioning, semaphore-based bulkheads, y resource quotas.
GuideGuía completa de arquitectura RabbitMQ
Diseña y opera RabbitMQ para mensajería confiable. Cubre exchanges, queues, bindings, patrones de routing, dead letter queues, clustering y mejores prácticas de producción para workloads de alto throughput.
PatternPatrón Saga
Gestiona transacciones distribuidas a través de múltiples servicios encadenando transacciones locales con acciones compensatorias para rollbacks. Un patrón de microservicios.
PatternPatrón de Consumidor Idempotente
Procesa mensajes de una cola exactamente una vez sin importar los duplicados, usando operaciones idempotentes, identificadores únicos y estrategias de deduplicación.