En pocas palabras
Conjunto de patrones que limitan el impacto de dependencias lentas o caídas para que un fallo local no se convierta en una caída en cascada.
Qué problema resuelve
Un proveedor lento agota hilos y conexiones, las peticiones se acumulan y todo el sistema deja de responder.
Cuándo utilizarlo
Cualquier llamada por red. Ejemplo: la consulta al buró de crédito tarda 30 s y bloquea las solicitudes de todos los clientes.
De la idea a la práctica
Funcionamiento, ejemplos y decisiones para pasar al siguiente nivel.
Los patrones y lo que resuelven
| Patrón | Protege contra | Idea |
|---|---|---|
| Timeout | Esperas indefinidas | Toda llamada tiene un límite; mejor fallar rápido |
| Retry con backoff | Fallos transitorios | Reintentar pocas veces, esperando cada vez más y con jitter |
| Circuit breaker | Insistir sobre una dependencia caída | Tras N fallos, cortar llamadas un tiempo y probar después |
| Bulkhead | Que una dependencia consuma todos los recursos | Pools separados por dependencia (hilos, conexiones) |
| Rate limiter | Sobrecarga propia o de terceros | Limitar peticiones por unidad de tiempo |
| Load shedding | Colapso por exceso de carga | Rechazar pronto (503/429) lo que no se puede atender |
| Fallback | Indisponibilidad total | Respuesta degradada: caché, valor por defecto, cola diferida |
Timeouts y presupuesto de latencia
Fija el timeout a partir de la latencia observada (p. ej. algo por encima del p99) y del presupuesto total del caso de uso. Si la API tiene 2 s de SLO y llama a tres dependencias en serie, ninguna puede tener 5 s de timeout. Propaga un deadline (gRPC lo hace de forma nativa) para que los servicios aguas abajo dejen de trabajar en peticiones que ya nadie espera.
Reintentos sin amplificar el problema
- Solo sobre errores transitorios (timeouts,
503,429) y operaciones idempotentes. - Backoff exponencial con jitter:
espera = random(0, min(tope, base · 2^intento)). Sin jitter, todos los clientes reintentan a la vez. - Reintenta en una sola capa. Tres capas con tres reintentos cada una multiplican la carga por 27.
- Usa un retry budget: no más de, por ejemplo, el 10 % de las peticiones pueden ser reintentos.
Circuit breaker
fallos ≥ umbral
CERRADO ──────────────────▶ ABIERTO ──(espera)──▶ SEMIABIERTO
▲ │
└──────────── llamadas de prueba con éxito ───────┘
(si fallan, vuelve a ABIERTO)
En estado abierto la llamada falla inmediatamente, lo que libera recursos y da tiempo a la dependencia para recuperarse. Ver la implementación en Resilience4j.
Ejemplo: consulta al buró
| Decisión | Valor ilustrativo |
|---|---|
| Timeout | 3 s (p99 observado: 1,8 s) |
| Reintentos | 1, solo en 503/timeout, backoff 200 ms + jitter |
| Circuit breaker | Abre con 50 % de fallos en 20 llamadas; semiabierto a los 30 s |
| Bulkhead | Máximo 20 llamadas concurrentes al buró |
| Fallback | Solicitud pasa a PENDIENTE_BURO y se reevalúa desde una cola |
El fallback es una decisión de negocio: aprobar sin buró no es aceptable, pero dejar la solicitud pendiente y avisar al cliente sí.
Ficha de resiliencia por dependencia
| Dependencia | Tipo | Timeout | Reintentos | Circuit breaker | Bulkhead | Degradación |
|---|---|---|---|---|---|---|
| Buró de crédito | Externa | 3 s | 1, solo 503/timeout | 50 % en 20 llamadas | 20 | Solicitud PENDIENTE_BURO |
| PostgreSQL | Interna | 2 s | 0 (transacción) | No | Pool 10 | Error 503 con Retry-After |
| Servicio de SMS | Externa | 2 s | 3 con backoff | Sí | 10 | Encolar y reenviar |
| Motor de scoring | Interna | 800 ms | 1 | Sí | 30 | Reglas básicas + revisión manual |
Mantener esta tabla junto al código (o en el ADR correspondiente) obliga a decidir explícitamente qué pasa cuando cada dependencia falla.
Timeouts en cascada
Cliente (timeout 10 s)
└─ API pública (presupuesto 8 s)
├─ Scoring (800 ms)
├─ Buró (3 s, 1 reintento → máx. ~6,5 s)
└─ BD (2 s)
Cada capa debe tener un timeout menor que la que la llama. Si la API pública esperara 30 s al buró, el cliente ya habría abandonado y la API seguiría ocupando un hilo y una conexión por una respuesta que nadie leerá.
Load shedding y concurrencia adaptativa
Cuando la carga supera la capacidad, es mejor rechazar rápido una parte que degradar a todos:
- Limitar peticiones concurrentes en curso y responder
503o429al superar el límite. - Priorizar: atender antes los pagos que las consultas de catálogo.
- Concurrencia adaptativa: ajustar el límite según la latencia observada (algoritmos tipo AIMD o Vegas, disponibles en librerías como Netflix concurrency-limits).
Probar la resiliencia
# Toxiproxy: insertar 5 s de latencia en la conexión hacia el mock del buró
toxiproxy-cli create -l 0.0.0.0:18080 -u buro-mock:8080 buro
toxiproxy-cli toxic add -t latency -a latency=5000 buro
# Ejecutar la prueba de integración y comprobar: respuesta < 4 s, estado PENDIENTE_BURO,
# circuito abierto tras N fallos y métrica de fallback incrementada
toxiproxy-cli toxic remove -n latency_downstream buro
| Experimento | Resultado esperado |
|---|---|
| Latencia alta en una dependencia | Timeout y degradación; el resto del servicio sigue sano |
| Dependencia devuelve 503 continuamente | El circuito se abre; no hay tormenta de reintentos |
| Pico de tráfico 3× | Rechazos controlados; latencia de las peticiones aceptadas estable |
| Caída de una zona | El tráfico se redistribuye sin intervención |
Empieza en entornos de prueba con Toxiproxy o WireMock y, con madurez, ejecuta game days controlados en producción.
Criterios de implementación
Inventaría las dependencias de cada servicio y define para cada una timeout, reintentos y comportamiento degradado. Mide aperturas del circuito y rechazos como métricas de primera clase. Valida el diseño con experimentos de inyección de fallos (latencia y errores) en un entorno de pruebas antes de confiar en él en producción.
Continúa en las fuentes
Documentación oficial para verificar detalles y explorar más.