Timeout, Retry y Circuit Breaker: orden correcto, configuración y errores comunes

Una llamada tiene tres reintentos. El servicio intermedio también. El cliente superior vuelve a intentar. Lo que parecía tolerancia a fallos termina amplificando una dependencia degradada.

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ónPregunta que respondeQué limitaQué no resuelve
Timeout¿Cuánto tiempo estoy dispuesto a esperar?Duración de una llamada o faseNo recupera una operación fallida
Retry¿Vale la pena realizar otro intento?Número y frecuencia de intentosNo 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 degradadaNo 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:

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:

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:

  1. Validación remota.
  2. Reserva de inventario.
  3. Persistencia.
  4. 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.

Texto
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:

  1. El SLO o límite funcional de la operación.
  2. La distribución histórica de latencia de la dependencia.
  3. El tiempo que necesitan las capas restantes.
  4. 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:

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:

No basta con que el error sea técnico. También deben cumplirse tres condiciones:

  1. Probabilidad de recuperación: repetir puede producir un resultado diferente.
  2. Seguridad semántica: repetir no genera efectos duplicados o inconsistentes.
  3. Presupuesto disponible: queda tiempo y capacidad para otro intento.

Candidatos habituales

Señal¿Reintentar?Condición
Reset de conexiónPosiblementeOperación segura y deadline suficiente
Timeout de redPosiblementeNo existe evidencia de efecto duplicado o hay idempotencia
HTTP 429PosiblementeRespetar Retry-After, cuota y presupuesto
HTTP 503PosiblementeLa dependencia declara condición temporal y existe margen
Conflicto transitorio conocidoPosiblementeLa semántica funcional permite repetir
Instancia no saludablePosiblementeEl 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 falloMotivo
Payload inválidoEl mismo contenido volverá a fallar
Validación funcionalNo cambia sin modificar la solicitud
Autorización denegadaRepetir no concede permisos
Recurso inexistenteNormalmente es un resultado definitivo
Operación no idempotente sin protecciónPuede duplicar efectos
Deadline agotadoEl resultado ya no es útil
Dependencia explícitamente saturada sin ventana de recuperaciónEl 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:

Texto
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:

HTTP
POST /orders
Idempotency-Key: 0f34d7b8-5f6e-4c23-a0a5-54e5f2a8a921

El servidor debe:

  1. Registrar la clave y el estado de la operación.
  2. Detectar repeticiones.
  3. Devolver el resultado previo o continuar una ejecución incompleta.
  4. Evitar volver a aplicar el efecto.
  5. 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:

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:

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:

Texto
100 ms -> 200 ms -> 400 ms -> 800 ms

Una forma común de expresarlo es:

Pseudocódigo
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:

Pseudocódigo
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

Retry storm: cuando la recuperación se convierte en carga

Una retry storm es un bucle de realimentación positiva:

  1. La dependencia se degrada.
  2. La latencia aumenta.
  3. Más clientes agotan su timeout.
  4. Los clientes reintentan.
  5. El tráfico efectivo supera al tráfico original.
  6. La dependencia pierde más capacidad.
  7. Aumentan nuevamente latencia y errores.
Bucle de una retry storm Ciclo cerrado en el que una dependencia degradada aumenta la latencia, provoca timeouts y reintentos, incrementa la carga efectiva, agota recursos y vuelve a degradar la dependencia. Dependencia degradada Sube la latencia Expiran timeouts Clientes reintentan Sube la carga efectiva Recursos y colas agotados realimentación positiva
Bucle de realimentación donde una dependencia degradada aumenta la latencia, provoca timeouts y retries, incrementa la carga y empeora nuevamente la degradación. El retry no crea capacidad: cuando la causa es saturación, cada intento adicional puede reducir la probabilidad de recuperación.

Señales observables

Una tormenta de reintentos suele aparecer como:

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:

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:

Texto
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:

Texto
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:

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:

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

Texto
Circuit Breaker
  └── Retry
        └── Timeout por intento
              └── Dependencia

Expresado como función:

Pseudocódigo
CircuitBreaker(
    Retry(
        Timeout(call)
    )
)

El breaker recibe un único resultado después de que el retry termina.

Ventajas

Riesgos

Cuándo puede ser defendible

Opción B: Circuit Breaker dentro del Retry

Texto
Retry
  └── Circuit Breaker
        └── Timeout por intento
              └── Dependencia

Expresado como función:

Pseudocódigo
Retry(
    CircuitBreaker(
        Timeout(call)
    )
)

Cada intento consulta y actualiza el breaker.

Ventajas

Riesgos

Cuándo puede ser defendible

Dos órdenes de composición, dos señales Comparación entre la Opción A, con el Circuit Breaker por fuera del Retry observando la operación lógica, y la Opción B, con el Circuit Breaker por dentro del Retry observando cada intento físico. Opción A el breaker ve la operación lógica Opción B el breaker ve cada intento Circuit Breaker Retry Timeout / intento Dependencia Retry Circuit Breaker Timeout / intento Dependencia
Comparación entre un Circuit Breaker exterior al Retry, que observa la operación lógica, y un Circuit Breaker interior, que observa cada intento. El wrapper exterior aparece arriba; mover el breaker cambia si registra un resultado agregado o cada intento físico. La elección debe coincidir con la señal que los umbrales pretenden representar.

Criterio de decisión

Antes de elegir el orden, responder:

  1. ¿Qué evento debe aumentar la tasa de fallos del breaker?
  2. ¿Un éxito después de retry debe ocultar los intentos fallidos?
  3. ¿Cuánta carga adicional tolera la dependencia?
  4. ¿El rechazo del breaker puede volver a reintentarse?
  5. ¿La política dispone de deadline y retry budget?
  6. ¿Las métricas distinguen operación lógica de intento físico?
  7. ¿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:

Texto
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:

Texto
1 intento inicial + 3 reintentos = 4 intentos totales

El máximo teórico contra la dependencia final es:

Texto
4 × 4 × 4 = 64 llamadas

Caso 2: tres intentos totales

Si la configuración significa tres intentos incluyendo el inicial:

Texto
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:

Texto
Llamadas máximas a la última dependencia = ∏ intentos_totales_por_capa

Si la herramienta configura retries adicionales:

Texto
intentos_totales = 1 + retries_adicionales
Amplificación de tres capas Una operación de usuario se multiplica por cuatro en cada capa: cuatro intentos en la primera, dieciséis en la segunda y hasta sesenta y cuatro llamadas en la dependencia final. 1 operación de usuario 4 intentos · capa A 16 intentos · capa B 64 llamadas · dependencia × 4 × 4 × 4
Una operación de usuario se convierte en cuatro intentos en la primera capa, dieciséis en la segunda y hasta sesenta y cuatro llamadas en la dependencia final. La cifra es un máximo teórico, pero basta para mostrar que los retries locales no son independientes.

Decisión operativa

La arquitectura debe declarar:

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:

Texto
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:

Texto
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:

Una política conceptual podría asignar:

Texto
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.

Presupuesto temporal dentro del deadline Cascada de dos segundos donde trabajo previo, dos intentos, un backoff con jitter y un margen de retorno se suceden dentro del deadline de 2,000 ms. Deadline: 2,000 ms 0 ms 2,000 ms Trabajo previo 300 ms Intento 1 500 ms Backoff + jitter 100 ms Intento 2 500 ms Retorno y margen 600 ms
Línea de tiempo de dos segundos con trabajo previo, dos intentos, un backoff y margen reservado para completar la respuesta. Los intentos y backoffs deben caber dentro del deadline completo, incluyendo trabajo previo y tiempo de retorno.

Pseudocódigo conceptual

El siguiente fragmento expresa decisiones, no una implementación lista para producción:

Pseudocódigo
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

Texto
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:

Configuración defendible

Ejemplo conceptual:

Texto
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:

Política peligrosa frente a política defendible A la izquierda, una política peligrosa sin deadline, con retries en varias capas, sin backoff ni jitter y con duplicados. A la derecha, una política defendible con deadline propagado, un único propietario del retry, backoff con jitter y budget, idempotencia y telemetría. Peligrosa Defendible Sin deadline Retries en varias capas Sin backoff ni jitter Duplicados y carga amplificada Deadline propagado Un propietario del retry Backoff + jitter + budget Idempotencia y telemetría
Comparación entre una política sin deadline y con retries múltiples, y otra con deadline propagado, retry controlado, idempotencia y telemetría. La diferencia no está en "activar" patrones, sino en compartir presupuesto, semántica y observabilidad.

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

Texto
Máximo de intentos totales: 2

Evita loops indefinidos y hace calculable el peor caso.

2. Presupuesto temporal

Texto
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:

Texto
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

Texto
retry_amplification_factor =
    physical_attempts / original_operations
Texto
retry_recovery_rate =
    operations_succeeded_after_retry / operations_with_retry
Texto
retry_budget_consumption =
    retries_executed / retries_allowed

Interpretación:

Cuándo no reintentar

No reintentar cuando:

  1. El error es permanente para la misma solicitud.
  2. La solicitud es inválida.
  3. La operación no es idempotente y no existe deduplicación.
  4. No queda tiempo suficiente en el deadline.
  5. El breaker está abierto y su rechazo no representa una condición reintentable dentro de la misma operación.
  6. La dependencia está saturada y no comunica una ventana razonable de recuperación.
  7. El intento es costoso y aumenta el daño potencial.
  8. La respuesta es incierta y repetir puede duplicar efectos.
  9. El contrato exige intervención humana o compensación.
  10. 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:

  1. ¿Qué observó el usuario?
  2. ¿Cuánto trabajo físico realizó el sistema?

Métricas

MétricaPropósito
Operaciones originalesDenominador real de demanda
Intentos físicosCarga enviada a la dependencia
RetriesIntentos adicionales
Intentos por operaciónAmplificación
Éxitos sin retrySalud normal
Éxitos después de retryValor recuperado
Fallos finalesResultado lógico
Timeouts por faseLocalización del límite agotado
Duración por intentoComportamiento de la dependencia
Duración end-to-endExperiencia del consumidor
Transiciones del breakerClosed, Open y Half-Open
Llamadas rechazadasTráfico cortado antes de la dependencia
Slow-call rateDegradación sin error explícito
Consumo del retry budgetPresión adicional
Operaciones deduplicadasEfectos 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:

Texto
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:

Texto
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:

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 inyectadoResultado esperado
Reset de conexión en el primer intentoRetry solo si la operación es segura
503 con Retry-AfterEspera compatible con header y deadline
Latencia superior al timeoutCancelación y liberación de recursos
Respuesta perdida después del commitUn solo efecto mediante idempotencia
Dependencia al 100% de erroresBreaker abre y reduce llamadas físicas
Dependencia lenta sin erroresSlow-call rate detecta degradación
Oleada de clientes simultáneosJitter distribuye los reintentos
Retry budget agotadoNuevos retries se rechazan de forma observable
Deadline casi agotadoNo se inicia un intento sin tiempo suficiente
Recuperación parcialHalf-Open limita probes y evita reabrir todo el tráfico

Indicadores de éxito

La solución funcionó si:

Señales de que la política empeoró el sistema

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

Tiempo

Capacidad

Circuit Breaker

Observabilidad

Validació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

Jorel del Portal

Jorel del Portal

Ingeniero de sistemas especializado en arquitectura de software empresarial y plataformas de alta disponibilidad.