Con tres capas y cuatro intentos por capa, una sola operación puede producir hasta 64 llamadas al componente más profundo. Si cada intento espera un segundo y además introduce backoff, la respuesta puede seguir consumiendo recursos mucho después de haber perdido utilidad para el usuario.
La idea central es esta:
Timeout, Retry y Circuit Breaker no son protecciones independientes. Forman una política única de consumo de tiempo y capacidad.
Este artículo explica qué debe limitar cada patrón, cómo decidir el orden de composición, qué errores pueden reintentarse, cómo evitar tormentas de reintentos y qué señales permiten comprobar que la política funciona en producción.
Qué problema resuelve cada patrón
Los tres patrones actúan sobre una llamada remota, pero no protegen contra el mismo riesgo.
| Patrón | Pregunta que responde | Qué limita | Qué no resuelve |
|---|---|---|---|
| Timeout | ¿Cuánto tiempo estoy dispuesto a esperar? | Duración de una llamada o fase | No recupera una operación fallida |
| Retry | ¿Vale la pena realizar otro intento? | Número y frecuencia de intentos | No limita por sí solo el tiempo total ni la carga |
| Circuit Breaker | ¿Debo seguir enviando tráfico a esta dependencia? | Acceso temporal a una dependencia degradada | No sustituye timeouts, control de concurrencia ni capacidad |
Un timeout evita que una llamada ocupe recursos indefinidamente. Un retry intenta recuperar fallos transitorios. Un Circuit Breaker deja de insistir cuando la evidencia indica que nuevas llamadas tienen baja probabilidad de éxito.
El error de diseño aparece cuando cada patrón se configura de forma aislada:
- El timeout se define sin considerar cuántos intentos caben.
- El retry se activa en varias capas.
- El Circuit Breaker observa una señal distinta de la que el equipo cree estar midiendo.
- La solicitud reinicia su presupuesto de tiempo en cada salto.
- La operación se repite sin una garantía de idempotencia.
Ninguno de estos patrones elimina la necesidad de capacidad suficiente, límites de concurrencia y observabilidad. Un Circuit Breaker no es un bulkhead; puede rechazar nuevas llamadas cuando está abierto, pero no necesariamente limita cuántas llamadas simultáneas atraviesan el circuito cuando está cerrado.
Timeout: limitar la espera y preservar capacidad
Una espera sin límite no es solo un problema de experiencia de usuario. Mientras una solicitud espera, puede retener threads, conexiones, memoria, slots de un pool, locks, buffers o capacidad de ejecución. Cuando el volumen crece, una dependencia lenta puede convertir la espera en agotamiento de recursos.
Timeout por intento
El timeout por intento limita una interacción física concreta con la dependencia.
Ejemplos:
- Una solicitud HTTP.
- Una llamada gRPC.
- Una consulta a una base de datos.
- Una operación contra una cola o un servicio externo.
No debe asumirse que una única configuración cubre todas las fases de la comunicación. Según la librería, pueden existir límites separados para resolución DNS, establecimiento de conexión, handshake TLS, adquisición de conexión desde un pool, escritura, lectura y duración total. La única forma de saber qué se está limitando es revisar la semántica real del cliente utilizado y comprobarla mediante pruebas.
Timeout por operación
Una operación lógica puede contener más de un intento y más de una llamada.
Por ejemplo, registrarOrden puede ejecutar:
- Validación remota.
- Reserva de inventario.
- Persistencia.
- Publicación de un evento.
Un timeout de 400 ms en cada llamada no implica que la operación completa dure 400 ms. Si existen llamadas secuenciales, retries, esperas de backoff y colas internas, el tiempo end-to-end será mayor.
Deadline end-to-end
El deadline representa el instante después del cual el resultado deja de ser útil para el consumidor. A diferencia de un timeout local, debe acompañar a la solicitud a través de las capas.
Deadline total: 2,000 ms
Tiempo consumido en la primera capa: 700 ms
Tiempo restante: 1,300 ms
La siguiente capa no debería iniciar otro reloj de 2,000 ms. Debe trabajar con los 1,300 ms restantes y reservar margen para devolver la respuesta.
gRPC distingue entre timeout como duración máxima y deadline como punto temporal, y documenta la propagación del tiempo restante a llamadas posteriores. Esta propagación evita que cada servicio reinicie el presupuesto y continúe trabajando después de que el cliente original haya abandonado la solicitud.
Decisión operativa
El timeout debe surgir de cuatro entradas:
- El SLO o límite funcional de la operación.
- La distribución histórica de latencia de la dependencia.
- El tiempo que necesitan las capas restantes.
- El costo de cortar demasiado pronto frente al costo de esperar demasiado.
Usar únicamente la media es peligroso. Para diseñar el presupuesto se necesitan los percentiles de latencia de la dependencia, segmentados por operación, región, tamaño de payload y condición de carga.
Un timeout demasiado bajo genera falsos fallos y retries innecesarios. Uno demasiado alto mantiene recursos ocupados, incrementa la cola y retrasa la detección de una degradación.
Validación
Una configuración de timeout no queda validada porque "no produjo errores" durante una prueba funcional. Debe comprobarse bajo:
- Carga normal.
- Carga cercana al límite.
- Latencia inducida.
- Pérdida o reset de conexión.
- Saturación del pool.
- Respuesta parcial o lenta.
- Dependencia completamente inaccesible.
La métrica principal no es solo timeout_count. También debe medirse cuánto trabajo continuó ejecutándose después de que el consumidor agotó su deadline.
Retry: repetir solo cuando existe una posibilidad razonable de éxito
Un retry solo tiene sentido cuando el siguiente intento puede observar una condición distinta de la anterior.
Esto ocurre, por ejemplo, cuando:
- Una conexión fue reiniciada de forma transitoria.
- Un paquete se perdió.
- La solicitud llegó a una instancia defectuosa y el siguiente intento puede ir a otra.
- Un servicio responde temporalmente con sobrecarga y comunica cuándo volver a intentar.
- Existe un conflicto transitorio cuya semántica permite repetir la operación.
No basta con que el error sea técnico. También deben cumplirse tres condiciones:
- Probabilidad de recuperación: repetir puede producir un resultado diferente.
- Seguridad semántica: repetir no genera efectos duplicados o inconsistentes.
- Presupuesto disponible: queda tiempo y capacidad para otro intento.
Candidatos habituales
| Señal | ¿Reintentar? | Condición |
|---|---|---|
| Reset de conexión | Posiblemente | Operación segura y deadline suficiente |
| Timeout de red | Posiblemente | No existe evidencia de efecto duplicado o hay idempotencia |
| HTTP 429 | Posiblemente | Respetar Retry-After, cuota y presupuesto |
| HTTP 503 | Posiblemente | La dependencia declara condición temporal y existe margen |
| Conflicto transitorio conocido | Posiblemente | La semántica funcional permite repetir |
| Instancia no saludable | Posiblemente | El balanceador puede seleccionar otro destino |
429 Too Many Requests y 503 Service Unavailable no significan "reintentar inmediatamente". HTTP permite que el servidor indique un tiempo con Retry-After. Ignorar esa señal y ejecutar un retry inmediato puede aumentar la sobrecarga que originó la respuesta.
Errores que normalmente no deben reintentarse
| Tipo de fallo | Motivo |
|---|---|
| Payload inválido | El mismo contenido volverá a fallar |
| Validación funcional | No cambia sin modificar la solicitud |
| Autorización denegada | Repetir no concede permisos |
| Recurso inexistente | Normalmente es un resultado definitivo |
| Operación no idempotente sin protección | Puede duplicar efectos |
| Deadline agotado | El resultado ya no es útil |
| Dependencia explícitamente saturada sin ventana de recuperación | El retry añade carga sin aumentar probabilidad de éxito |
Estas categorías no son universales. Un 404 puede ser transitorio en un sistema con consistencia eventual; un 409 puede representar un conflicto recuperable; un 500 puede ser permanente para un payload específico. La política debe basarse en la semántica del contrato, no solo en la familia del código HTTP.
Propietario del retry
Debe existir un propietario claro del retry.
En una cadena cliente -> API -> servicio -> base de datos, no es razonable activar retries automáticos en cada salto sin un presupuesto coordinado. El nivel más cercano a la dependencia conoce mejor sus errores técnicos; el nivel superior conoce mejor si la operación completa sigue siendo útil. La decisión debe hacer explícita esa tensión.
Una política defendible suele elegir un nivel principal para reintentar y obliga al resto a propagar errores distinguibles, deadlines y señales de sobrecarga.
Idempotencia: el requisito que evita efectos duplicados
Una operación es idempotente cuando repetir la misma intención produce el mismo efecto observable que ejecutarla una sola vez.
El caso crítico no es cuando el servidor rechaza la solicitud. Es cuando la ejecuta y la respuesta se pierde:
1. El cliente envía la operación.
2. El servidor la procesa y confirma el cambio.
3. La respuesta se pierde.
4. El cliente observa un timeout.
5. El cliente reintenta.
Desde el cliente, el resultado es incierto. Desde el servidor, el primer intento pudo haber terminado correctamente.
Idempotencia HTTP no equivale a idempotencia funcional
RFC 9110 considera idempotentes, entre otros, los métodos PUT, DELETE y los métodos seguros. También advierte que una solicitud no idempotente no debe reintentarse automáticamente salvo que el cliente conozca que la semántica real es idempotente o pueda determinar que el primer intento no fue aplicado.
Eso no significa que todo PUT sea seguro por implementación ni que todo POST sea imposible de reintentar. La seguridad depende del contrato real.
Idempotency key
Una estrategia frecuente consiste en asociar una clave única a la intención:
POST /orders
Idempotency-Key: 0f34d7b8-5f6e-4c23-a0a5-54e5f2a8a921
El servidor debe:
- Registrar la clave y el estado de la operación.
- Detectar repeticiones.
- Devolver el resultado previo o continuar una ejecución incompleta.
- Evitar volver a aplicar el efecto.
- Retener el registro durante una ventana mayor que el horizonte máximo de retries.
La clave no debe reutilizarse para payloads diferentes. El servidor puede almacenar un hash normalizado de la solicitud y rechazar una repetición incompatible.
Dedupe y estado de operación
La deduplicación puede implementarse en distintos niveles:
- Restricción única en la base de datos.
- Registro de comandos recibidos.
- Tabla de idempotencia.
- Identificador funcional estable.
- Máquina de estados de la operación.
- Inbox transaccional para consumidores de mensajes.
La elección depende del efecto que debe protegerse. Una clave almacenada en memoria no evita duplicados después de un reinicio. Una tabla sin política de expiración crece indefinidamente. Una ventana demasiado corta permite que un retry tardío vuelva a ejecutar la operación.
Validación
La prueba mínima debe inyectar la pérdida de respuesta después del commit y antes de que el cliente reciba la confirmación. El resultado esperado es:
- Un solo efecto persistido.
- Dos o más intentos observables.
- Una respuesta consistente para la misma clave.
- Ninguna publicación duplicada o, si el sistema es al menos una vez, consumidores capaces de deduplicar.
Backoff y jitter: separar y desincronizar los intentos
Reintentar inmediatamente supone que la condición que causó el fallo desapareció en unos pocos microsegundos. En una dependencia saturada, esa suposición suele ser falsa.
Exponential backoff
El backoff incrementa progresivamente el tiempo entre intentos.
Un esquema conceptual:
100 ms -> 200 ms -> 400 ms -> 800 ms
Una forma común de expresarlo es:
delay_n = min(max_delay, base_delay × 2^(n-1))
El límite máximo evita pausas indefinidas. El deadline debe prevalecer: si la espera más el tiempo mínimo de otro intento ya no cabe, no se debe reintentar.
Jitter
Si miles de clientes usan exactamente la misma secuencia, todos pueden reintentar al mismo tiempo. El jitter agrega aleatoriedad para romper esa sincronización.
Ejemplo de full jitter:
cap_n = min(max_delay, base_delay × 2^(n-1))
delay_n = random(0, cap_n)
El backoff reduce la frecuencia. El jitter reduce la correlación temporal.
Google SRE recomienda backoff exponencial aleatorizado y advierte que retries sincronizados pueden formar ondas que se amplifican. AWS documenta el mismo problema en su guía de timeouts, retries, backoff y jitter.
Qué puede salir mal
- Backoff sin jitter: reduce frecuencia, pero conserva picos sincronizados.
- Jitter sin límite: puede consumir el deadline antes del siguiente intento.
- Backoff reiniciado en cada capa: vuelve a multiplicar la duración.
- Ignorar
Retry-After: contradice la señal explícita del servidor. - Secuencia determinista por proceso: múltiples instancias iniciadas juntas pueden seguir sincronizadas.
- Retry inmediato en errores de sobrecarga: convierte el mecanismo de recuperación en tráfico adicional.
Retry storm: cuando la recuperación se convierte en carga
Una retry storm es un bucle de realimentación positiva:
- La dependencia se degrada.
- La latencia aumenta.
- Más clientes agotan su timeout.
- Los clientes reintentan.
- El tráfico efectivo supera al tráfico original.
- La dependencia pierde más capacidad.
- Aumentan nuevamente latencia y errores.
Señales observables
Una tormenta de reintentos suele aparecer como:
- QPS a la dependencia mayor que el volumen de solicitudes originales.
- Aumento de
attempts_per_request. - Crecimiento simultáneo de latencia, timeouts y retries.
- Caída de la proporción de éxitos recuperados por retry.
- Saturación de pools, colas o conexiones.
- Tráfico que permanece alto aunque la demanda de usuarios no haya crecido.
- Rechazos del retry budget o del Circuit Breaker.
El error frecuente durante un incidente es interpretar los retries como un síntoma secundario. Pueden ser parte de la causa que mantiene la plataforma fuera de capacidad.
Circuit Breaker: dejar de llamar cuando insistir aumenta el daño
El Circuit Breaker resume el comportamiento reciente de una dependencia y decide si una nueva llamada debe ejecutarse.
Sus estados básicos son:
Closed: permite llamadas y registra sus resultados.Open: rechaza llamadas sin alcanzar la dependencia.Half-Open: permite un conjunto limitado de llamadas de prueba para evaluar recuperación.
Un breaker puede considerar tasa de fallos, llamadas lentas, una ventana mínima de observaciones y excepciones específicas. Frameworks como Resilience4j separan la tasa de errores de la tasa de llamadas lentas y requieren un mínimo de muestras antes de calcularlas.
El Circuit Breaker no limita necesariamente la concurrencia mientras está cerrado. Si el riesgo es agotar threads, conexiones o requests simultáneos, también se necesita un límite de concurrencia o bulkhead.
La explicación completa de estados, ventanas y recuperación pertenece al artículo de Circuit Breaker y fallos en cascada. Aquí interesa una decisión distinta: qué llamada ve el breaker cuando también existen retries.
Qué debe observar el Circuit Breaker
Antes de configurar umbrales hay que definir la unidad observada.
Opción 1: observar la operación lógica
Una operación con tres intentos puede registrarse como:
Intento 1: timeout
Intento 2: 503
Intento 3: éxito
Resultado lógico: éxito
Si el breaker solo observa el resultado lógico, registra un éxito. Esto evita abrir por un fallo transitorio que el retry recuperó, pero oculta dos fallos físicos y la carga adicional.
Opción 2: observar cada intento físico
El mismo flujo produce:
Fallo
Fallo
Éxito
El breaker ve tres llamadas. Detecta antes una degradación y puede abrir durante la secuencia, pero su tasa de fallos ya no representa operaciones de usuario. También puede abrirse demasiado rápido si la política fue diseñada pensando en resultados finales.
Opción 3: observar dos niveles
En sistemas con suficiente madurez operativa puede mantenerse:
- Una métrica o breaker técnico por intento contra la dependencia.
- Una métrica de resultado lógico después de retries.
- Un límite separado de retry budget.
- Un SLO basado en la experiencia end-to-end.
No siempre se necesitan dos breakers. Sí se necesitan dos conjuntos de señales: operaciones originales e intentos físicos.
Errores y llamadas lentas
Un servicio puede responder sin errores y aun así destruir el presupuesto de latencia. Por eso el breaker puede considerar:
- Tasa de fallos.
- Tasa de slow calls.
- Duración a partir de la cual una llamada se considera lenta.
- Número mínimo de llamadas antes de tomar una decisión.
- Excepciones ignoradas.
- Excepciones contadas como fallo.
- Ventana temporal o por cantidad.
El umbral de slow call no debe copiarse de una configuración genérica. Debe derivarse del presupuesto de la operación y de la distribución real de latencia.
Orden de composición
No existe un orden universal porque el orden cambia la unidad que observa cada política.
La notación usada a continuación muestra el wrapper exterior primero.
Opción A: Retry dentro del Circuit Breaker
Circuit Breaker
└── Retry
└── Timeout por intento
└── Dependencia
Expresado como función:
CircuitBreaker(
Retry(
Timeout(call)
)
)
El breaker recibe un único resultado después de que el retry termina.
Ventajas
- Mide el éxito o fallo de la operación lógica.
- Un fallo transitorio recuperado no abre el circuito.
- La semántica se aproxima a lo que observa el consumidor.
Riesgos
- Los intentos intermedios quedan ocultos para el breaker.
- La dependencia puede recibir varios intentos antes de registrar un fallo.
- Un breaker configurado solo con fallos finales puede reaccionar tarde.
- La tasa de éxito puede parecer saludable aunque el costo por operación se haya multiplicado.
Cuándo puede ser defendible
- Pocos intentos totales.
- Retry budget estricto.
- Operaciones idempotentes.
- Métricas explícitas por intento.
- Dependencia con capacidad para absorber fallos transitorios breves.
- Breaker con slow-call rate o señal de saturación complementaria.
Opción B: Circuit Breaker dentro del Retry
Retry
└── Circuit Breaker
└── Timeout por intento
└── Dependencia
Expresado como función:
Retry(
CircuitBreaker(
Timeout(call)
)
)
Cada intento consulta y actualiza el breaker.
Ventajas
- Los fallos físicos influyen inmediatamente en el estado.
- Si el breaker se abre, intentos posteriores pueden cortarse sin llegar a la dependencia.
- La señal se acerca al impacto real sobre el backend.
Riesgos
- La tasa de fallos representa intentos, no operaciones de usuario.
- Un conjunto pequeño de solicitudes con varios retries puede llenar rápidamente la ventana.
- El retry puede intentar de nuevo después de recibir un rechazo del breaker si ese rechazo fue clasificado erróneamente como retriable.
- El breaker puede abrirse ante fallos transitorios que el sistema habría recuperado.
Cuándo puede ser defendible
- La dependencia es sensible a carga adicional.
- Cada fallo físico es relevante para capacidad.
- El rechazo del breaker se clasifica como no retriable dentro de la misma operación.
- La ventana y el mínimo de llamadas se calibran con tráfico real.
- Existe telemetría separada para resultados lógicos.
Criterio de decisión
Antes de elegir el orden, responder:
- ¿Qué evento debe aumentar la tasa de fallos del breaker?
- ¿Un éxito después de retry debe ocultar los intentos fallidos?
- ¿Cuánta carga adicional tolera la dependencia?
- ¿El rechazo del breaker puede volver a reintentarse?
- ¿La política dispone de deadline y retry budget?
- ¿Las métricas distinguen operación lógica de intento físico?
- ¿La degradación principal aparece como errores, latencia o saturación?
La respuesta correcta no es "Retry antes" o "Circuit Breaker antes". Es una composición cuya semántica pueda explicarse, medirse y probarse.
Cómo se multiplican las llamadas entre capas
Supongamos tres capas:
Cliente -> Servicio A -> Servicio B -> Base de datos
Cada capa llama una vez a la siguiente y aplica la misma política.
Caso 1: tres retries adicionales
"Tres retries" significa:
1 intento inicial + 3 reintentos = 4 intentos totales
El máximo teórico contra la dependencia final es:
4 × 4 × 4 = 64 llamadas
Caso 2: tres intentos totales
Si la configuración significa tres intentos incluyendo el inicial:
3 × 3 × 3 = 27 llamadas
La diferencia no es lingüística. Cambia el factor de amplificación de 27 a 64.
Google SRE usa precisamente el ejemplo de tres capas con cuatro intentos por capa para mostrar cómo una acción puede producir 64 intentos contra una base de datos sobrecargada.
Fórmula general
Para n capas:
Llamadas máximas a la última dependencia = ∏ intentos_totales_por_capa
Si la herramienta configura retries adicionales:
intentos_totales = 1 + retries_adicionales
Decisión operativa
La arquitectura debe declarar:
- Cuántos intentos totales existen, no solo cuántos retries.
- Qué capa es propietaria del retry.
- Qué clientes o proxies ya reintentan automáticamente.
- Si el SDK incorpora retries por defecto.
- Si el service mesh o gateway añade otra política.
- Cómo se evita que un retry superior repita una operación que ya agotó su presupuesto inferior.
Durante una revisión de arquitectura, "tenemos tres retries" es una respuesta incompleta. La pregunta correcta es: ¿cuántos intentos físicos puede generar una operación end-to-end en el peor caso?
Cómo calcular el costo temporal
Para una sola capa con N intentos:
Tiempo máximo aproximado =
Σ timeouts_por_intento
+ Σ backoffs
+ overhead
El overhead incluye adquisición de conexiones, scheduling, serialización, colas, balanceo y trabajo local no cubierto por el timeout.
Ejemplo
Supuestos:
- Timeout por intento: 1,000 ms.
- Intentos totales: 4.
- Backoffs: 100 ms, 200 ms y 400 ms.
- Overhead omitido para simplificar.
Tiempo de intentos = 4 × 1,000 ms = 4,000 ms
Backoff acumulado = 100 + 200 + 400 = 700 ms
Tiempo potencial = 4,700 ms
Si el deadline del usuario es 2,000 ms, la política es internamente contradictoria. Aunque las librerías permitan configurar esos valores, no existe tiempo suficiente para ejecutarlos.
Presupuesto con deadline
Supuestos:
- Deadline end-to-end: 2,000 ms.
- Tiempo consumido antes de la dependencia: 300 ms.
- Margen de retorno y procesamiento final: 200 ms.
- Tiempo disponible para la dependencia: 1,500 ms.
Una política conceptual podría asignar:
Intento 1: máximo 500 ms
Backoff con jitter: hasta 100 ms
Intento 2: máximo 500 ms
Margen no asignado: 400 ms
El margen absorbe variaciones de scheduling, red y procesamiento. No es capacidad desperdiciada: evita diseñar una política que solo funciona cuando todos los componentes consumen exactamente su máximo teórico.
Pseudocódigo conceptual
El siguiente fragmento expresa decisiones, no una implementación lista para producción:
deadline = operation_deadline
attempt = 0
while attempt < max_total_attempts:
remaining = deadline - now()
if remaining <= minimum_completion_margin:
return DEADLINE_EXHAUSTED
if circuit_breaker_rejects():
return DEPENDENCY_UNAVAILABLE
per_attempt_timeout = min(
configured_attempt_timeout,
remaining - minimum_completion_margin
)
result = call_dependency(timeout=per_attempt_timeout)
attempt += 1
if result.success:
return result
if not is_retryable(result):
return result
if not operation_is_retry_safe:
return INDETERMINATE_RESULT
delay = jittered_backoff(attempt)
if delay + minimum_attempt_time >= deadline - now():
return result
sleep(delay)
return LAST_RESULT
La implementación real debe considerar cancelación, propagación de contexto, métricas, concurrencia, idempotencia, códigos del framework y comportamiento del breaker.
Configuración peligrosa frente a configuración defendible
Configuración peligrosa
Timeout por intento: 5 s
Retries: 3 adicionales
Backoff: inexistente
Jitter: inexistente
Retries en cliente, gateway y servicio
Operación no idempotente
Circuit Breaker: solo errores finales
Slow-call threshold: inexistente
Deadline end-to-end: inexistente
Esta política puede producir:
- Cuatro intentos consecutivos de cinco segundos por capa.
- Reintentos sincronizados.
- Multiplicación entre cliente, gateway y servicio.
- Efectos duplicados.
- Un breaker que permanece cerrado mientras los retries recuperan parcialmente.
- Trabajo que continúa aunque el usuario ya abandonó.
- Colas y pools ocupados por operaciones sin valor.
Configuración defendible
Ejemplo conceptual:
Deadline end-to-end: 2 s
Timeout por intento: hasta 400 ms, limitado por tiempo restante
Intentos totales: 2
Backoff: exponencial con jitter
Retry: solo errores transitorios clasificados
Retry-After: respetado cuando aplique
Idempotencia: garantizada
Propietario del retry: una capa definida
Circuit Breaker: failure rate + slow-call rate
Mínimo de muestras: calibrado con volumen
Retry budget: límite por tráfico y concurrencia
Telemetría: operaciones, intentos y tiempo total
Estos valores no son una receta. Solo muestran coherencia interna. La configuración final debe derivarse de:
- SLO y deadline de la operación.
- Latencia histórica y percentiles.
- Capacidad disponible en la dependencia.
- Semántica de los errores.
- Costo de duplicar la operación.
- Duración esperada de la condición transitoria.
- Comportamiento de SDK, proxy, gateway y service mesh.
- Estrategia de degradación funcional.
Presupuesto de retries
Los retries deben consumir una fracción controlada de la capacidad, no convertirse en una segunda carga invisible.
Un retry budget puede imponer límites complementarios.
1. Presupuesto por solicitud
Máximo de intentos totales: 2
Evita loops indefinidos y hace calculable el peor caso.
2. Presupuesto temporal
No iniciar otro intento si no cabe dentro del deadline restante.
Evita resultados tardíos y trabajo inútil.
3. Presupuesto de tráfico
Ejemplo ilustrativo:
Solicitudes originales por minuto: 10,000
Presupuesto de retries: 8%
Retries permitidos: 800 por minuto
El porcentaje no es universal. Debe ser menor que el headroom real de la dependencia y considerar que el tráfico original puede crecer durante el incidente.
4. Presupuesto de concurrencia
Limita cuántos retries pueden estar activos simultáneamente. Envoy, por ejemplo, permite expresar el retry budget como un porcentaje de solicitudes activas y pendientes, además de un mínimo de concurrencia.
5. Presupuesto por cliente o tenant
Evita que un consumidor defectuoso agote la capacidad de retries compartida.
Métricas derivadas
retry_amplification_factor =
physical_attempts / original_operations
retry_recovery_rate =
operations_succeeded_after_retry / operations_with_retry
retry_budget_consumption =
retries_executed / retries_allowed
Interpretación:
- Un factor de amplificación creciente indica más costo por operación.
- Una tasa de recuperación decreciente indica que los retries están dejando de ser útiles.
- Un budget agotado indica que la política está protegiendo capacidad, pero también que la dependencia o la clasificación de errores requiere investigación.
Cuándo no reintentar
No reintentar cuando:
- El error es permanente para la misma solicitud.
- La solicitud es inválida.
- La operación no es idempotente y no existe deduplicación.
- No queda tiempo suficiente en el deadline.
- El breaker está abierto y su rechazo no representa una condición reintentable dentro de la misma operación.
- La dependencia está saturada y no comunica una ventana razonable de recuperación.
- El intento es costoso y aumenta el daño potencial.
- La respuesta es incierta y repetir puede duplicar efectos.
- El contrato exige intervención humana o compensación.
- El sistema ya superó el retry budget.
También debe evitarse el retry automático cuando la respuesta correcta es degradar funcionalidad, encolar el trabajo, devolver un estado pendiente o pedir al consumidor que consulte más tarde.
Instrumentación mínima
La política debe poder responder dos preguntas distintas:
- ¿Qué observó el usuario?
- ¿Cuánto trabajo físico realizó el sistema?
Métricas
| Métrica | Propósito |
|---|---|
| Operaciones originales | Denominador real de demanda |
| Intentos físicos | Carga enviada a la dependencia |
| Retries | Intentos adicionales |
| Intentos por operación | Amplificación |
| Éxitos sin retry | Salud normal |
| Éxitos después de retry | Valor recuperado |
| Fallos finales | Resultado lógico |
| Timeouts por fase | Localización del límite agotado |
| Duración por intento | Comportamiento de la dependencia |
| Duración end-to-end | Experiencia del consumidor |
| Transiciones del breaker | Closed, Open y Half-Open |
| Llamadas rechazadas | Tráfico cortado antes de la dependencia |
| Slow-call rate | Degradación sin error explícito |
| Consumo del retry budget | Presión adicional |
| Operaciones deduplicadas | Efectos repetidos evitados |
No mezclar en una misma serie "requests" e "attempts". Si el dashboard muestra una tasa de error sin aclarar el denominador, el equipo puede interpretar fallos físicos como fallos de usuario o viceversa.
Logs estructurados
Campos recomendados:
operation_id
idempotency_key_hash
attempt_number
max_total_attempts
retry_reason
retryable
backoff_ms
remaining_deadline_ms
attempt_timeout_ms
breaker_state
breaker_decision
dependency
outcome
elapsed_total_ms
Evitar registrar claves de idempotencia completas si pueden considerarse sensibles. Usar hash o identificadores seguros y aplicar controles de cardinalidad.
Trazas distribuidas
Para HTTP, OpenTelemetry define http.request.resend_count para indicar cuántas veces una solicitud fue reenviada. Cada envío físico puede representarse con su propio span según la instrumentación, manteniendo una relación clara con la operación lógica.
Una estructura útil:
Span: operación lógica
├── Span: intento 1
├── Evento: retry programado, reason=timeout, backoff_ms=87
└── Span: intento 2, resend_count=1
Atributos o eventos adicionales:
- Número de intento.
- Tiempo restante del deadline.
- Motivo del retry.
- Estado del breaker.
- Rechazo del breaker.
- Timeout aplicado.
- Resultado físico y resultado lógico.
El artículo de observabilidad con métricas, logs y trazas desarrolla cómo correlacionar estas señales. Para el diagnóstico paso a paso durante una degradación, revisa la investigación estructurada de incidentes.
Cómo validar la política
Una política de resiliencia debe validarse con fallos controlados, no solo con tráfico exitoso.
Matriz mínima de pruebas
| Escenario inyectado | Resultado esperado |
|---|---|
| Reset de conexión en el primer intento | Retry solo si la operación es segura |
503 con Retry-After | Espera compatible con header y deadline |
| Latencia superior al timeout | Cancelación y liberación de recursos |
| Respuesta perdida después del commit | Un solo efecto mediante idempotencia |
| Dependencia al 100% de errores | Breaker abre y reduce llamadas físicas |
| Dependencia lenta sin errores | Slow-call rate detecta degradación |
| Oleada de clientes simultáneos | Jitter distribuye los reintentos |
| Retry budget agotado | Nuevos retries se rechazan de forma observable |
| Deadline casi agotado | No se inicia un intento sin tiempo suficiente |
| Recuperación parcial | Half-Open limita probes y evita reabrir todo el tráfico |
Indicadores de éxito
La solución funcionó si:
- La duración end-to-end no supera sistemáticamente el deadline.
- Las operaciones canceladas dejan de consumir trabajo aguas abajo.
- El factor de amplificación permanece dentro del presupuesto.
- La dependencia recibe menos tráfico cuando está degradada.
- Los retries recuperan una proporción medible de fallos transitorios.
- No aparecen efectos duplicados.
- El breaker abre y se recupera con una muestra suficiente.
- Los rechazos del breaker no originan un nuevo loop de retries.
- Los dashboards distinguen solicitudes originales, intentos y fallos finales.
- La degradación funcional es predecible.
Señales de que la política empeoró el sistema
- Aumenta la disponibilidad aparente, pero también el P99 end-to-end.
- El backend recibe más QPS durante una caída.
- La mayoría de retries falla.
- El breaker abre y cierra repetidamente.
- El retry budget se agota con tráfico normal.
- Los clientes reportan timeout mientras el servidor continúa procesando.
- Las métricas muestran éxitos finales, pero el costo por operación crece.
- Aparecen duplicados o compensaciones frecuentes.
La latencia end-to-end debe evaluarse junto con éxito y disponibilidad. Una operación que termina correctamente después de diez segundos puede contar como éxito técnico y seguir siendo un fallo para el consumidor.
Checklist operativo
Semántica
- ¿La operación es idempotente?
- ¿Existe una idempotency key o deduplicación cuando se necesita?
- ¿Qué errores son realmente transitorios?
- ¿Qué respuestas son definitivas?
- ¿Un rechazo del breaker es retriable dentro de la misma operación?
- ¿La respuesta perdida después del commit está contemplada?
Tiempo
- ¿Existe un deadline end-to-end?
- ¿Se propaga el tiempo restante?
- ¿El timeout por intento cubre las fases correctas?
- ¿Cuántos intentos caben realmente?
- ¿Se reserva margen para devolver la respuesta?
- ¿El backoff puede consumir el deadline?
Capacidad
- ¿Qué capa es propietaria del retry?
- ¿El SDK ya reintenta automáticamente?
- ¿El gateway, proxy o service mesh añade retries?
- ¿Existe presupuesto por solicitud?
- ¿Existe presupuesto de tráfico o concurrencia?
- ¿La dependencia tiene headroom para el peor caso?
Circuit Breaker
- ¿Observa intentos o resultados lógicos?
- ¿Registra errores y slow calls?
- ¿Existe un mínimo de muestras?
- ¿Las excepciones ignoradas están definidas?
- ¿Half-Open limita las llamadas de prueba?
- ¿Se necesita también un bulkhead?
Observabilidad
- ¿Se distinguen operaciones originales de intentos?
- ¿Se mide el factor de amplificación?
- ¿Se mide el éxito después de retry?
- ¿Se registra el tiempo restante del deadline?
- ¿Las trazas muestran cada intento?
- ¿Se observan transiciones y rechazos del breaker?
- ¿El retry budget tiene alerta?
Validación
- ¿Se probaron errores, latencia, resets y sobrecarga?
- ¿Se probó pérdida de respuesta después del efecto?
- ¿Se verificó cancelación aguas abajo?
- ¿Se probó recuperación parcial?
- ¿Se comparó carga original contra carga física?
- ¿Existe una estrategia de degradación?
Preguntas frecuentes
¿Qué se aplica primero: Retry o Circuit Breaker?
Depende de qué evento deba observar el breaker.
Si el Circuit Breaker envuelve al Retry, suele registrar el resultado lógico después de todos los intentos. Si el Retry envuelve al Circuit Breaker, cada intento atraviesa y alimenta al breaker. La decisión debe considerar capacidad de la dependencia, semántica de los umbrales, retry budget y telemetría disponible.
¿Cuántos retries debo configurar?
No existe un número universal. Primero debe definirse el deadline, el timeout por intento, el backoff, el costo de la operación y el headroom de la dependencia. La configuración debe expresarse como intentos totales, porque "tres retries" puede significar cuatro intentos.
En una operación interactiva con deadline corto, uno o dos intentos totales pueden consumir todo el presupuesto. En procesos asíncronos, la política puede extenderse durante más tiempo, pero necesita persistencia, deduplicación y límites globales.
¿Qué diferencia existe entre timeout y deadline?
El timeout es una duración máxima aplicada a una llamada o fase. El deadline es el límite temporal de la operación completa. Cuando una solicitud atraviesa varios servicios, cada capa debe respetar el tiempo restante del deadline en lugar de reiniciar un timeout completo.
¿Qué es jitter?
Es una variación aleatoria aplicada al backoff. Evita que muchos clientes que fallaron al mismo tiempo vuelvan a intentar en el mismo instante. El backoff separa intentos; el jitter evita que queden sincronizados.
¿Cuándo una operación es idempotente?
Cuando repetir la misma intención produce el mismo efecto observable que ejecutarla una vez. La idempotencia puede provenir del contrato, de una clave de idempotencia, de una restricción única o de una estrategia de deduplicación. El método HTTP por sí solo no garantiza que la implementación sea correcta.
¿Puede un retry empeorar una caída?
Sí. Si la causa es sobrecarga, cada retry añade trabajo al componente que ya no tiene capacidad. Sin límites, backoff, jitter y un presupuesto global, los retries pueden transformar una degradación parcial en un fallo en cascada.
Conclusión
Timeout, Retry y Circuit Breaker deben diseñarse como una sola política.
El timeout limita una espera. El retry consume otra oportunidad. El Circuit Breaker decide si esa oportunidad debe llegar a la dependencia. Los tres compiten por el mismo deadline y por la misma capacidad.
Antes de activar un retry, calcula cuántos intentos físicos puede producir una operación completa. Antes de configurar un breaker, define si observa intentos o resultados lógicos. Antes de repetir una operación, garantiza idempotencia. Después de desplegar, mide solicitudes originales, intentos, latencia end-to-end, recuperación y amplificación.
La prueba final no es que el sistema reintente. Es que, durante una degradación, realice menos trabajo inútil, preserve capacidad y falle dentro de límites conocidos.
Fuentes técnicas
- Google SRE — Addressing Cascading Failures: sre.google/sre-book/addressing-cascading-failures
- Google SRE — Handling Overload: sre.google/sre-book/handling-overload
- AWS — Timeouts, retries, and backoff with jitter: builder.aws.com/content/timeouts-retries-and-backoff-with-jitter
- AWS — Making retries safe with idempotent APIs: aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs
- gRPC — Deadlines: grpc.io/docs/guides/deadlines
- IETF — RFC 9110: HTTP Semantics: datatracker.ietf.org/doc/html/rfc9110
- IETF — RFC 6585: Additional HTTP Status Codes: datatracker.ietf.org/doc/html/rfc6585
- Resilience4j — CircuitBreaker: resilience4j.readme.io/docs/circuitbreaker
- OpenTelemetry — Semantic conventions for HTTP spans: opentelemetry.io/docs/specs/semconv/http/http-spans
- Envoy — Circuit breakers (RetryBudget): envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto