Circuit breakers y aislamiento de fallos para APIs en producción

Un dependency lento no falla — encola. El **aislamiento de fallos con circuit breaker** evita que un upstream enfermo consuma cada thread, conexión y presupuesto de reintentos de la flota. El patrón funciona cuando umbrales, fallbacks y probes de recuperación son explícitos antes del outage.

Ingeniería8 min de lectura
ResilienciaSistemas distribuidosObservabilidadRespuesta a incidentesDiseño de API
Compartir

El proveedor de pagos devolvió HTTP 503 durante cuarenta segundos. El servicio de checkout reintentó tres veces por request. Cada reintento mantuvo una conexión treinta segundos. Los pools de conexión se saturaron. Endpoints de lectura no relacionados hicieron timeout porque cada worker quedó bloqueado esperando a un vendor que nadie podía arreglar desde dentro de la aplicación. El dependency no tumbaron el servicio — lo ocuparon. El aislamiento de fallos con circuit breaker existe para convertir el fallo de un dependency de un stall en toda la flota en una superficie de fast-fail acotada con fallback definido.

Un circuit breaker es una máquina de estados envuelta alrededor de llamadas salientes. Mientras está sano, las llamadas pasan (closed). Tras suficientes fallos o respuestas lentas, el breaker abre y rechaza llamadas nuevas al instante — sin espera de socket, sin tormenta de reintentos. Tras un cooldown, un goteo de probes half-open prueba la recuperación antes de cerrar de nuevo. El patrón tiene décadas. Las implementaciones en producción siguen fallando porque los equipos tratan la librería como la arquitectura en lugar de definir qué significa fallo, qué devolver cuando el circuito abre y quién recibe página cuando los probes half-open siguen fallando.

El aislamiento de fallos empieza con límites de blast radius

El aislamiento de fallos traza una línea alrededor de lo que un outage de dependency puede afectar. Sin esa línea, los reintentos propagan carga hacia atrás al caller, los thread pools se agotan y los health checks fallan en servicios que nunca llamaron al dependency roto directamente.

Isolation layerWhat it protectsTypical failure mode without it
Process / containerCPU and memory on one hostNoisy neighbor within the pod
Connection pool limitsThreads and socketsPool starvation from one slow peer
Circuit breakerLatency tail and error budgetRetry amplification across the fleet
Bulkhead (separate pools)Critical vs non-critical pathsCheckout blocked by analytics export
Timeout + deadlineEnd-to-end request budgetUnbounded waits that never surface as errors

Los circuit breakers se sitúan entre timeouts y bulkheads. Los timeouts acotan cuánto puede esperar una llamada. Los breakers acotan cuántas llamadas pueden esperar en total una vez el dependency está claramente unhealthy. Los bulkheads aseguran que la apertura del circuito de analytics no drene el pool usado para autenticación.

Un circuit breaker sin fallback es un 503 más rápido. Un fallback sin circuit breaker es uno más lento.

Definir el fallback antes de que el tráfico de producción golpee el dependency: respuesta cacheada, feature degradada, job async encolado o error explícito con mensaje visible al usuario. "Devolver null y esperar que upstream lo maneje" no es fallback — es un contrato no documentado que se convierte en incidente de integridad de datos en el próximo deploy.

Estados del circuit breaker y los umbrales que importan

La máquina de estados es simple. El tuning no.

Closed — todas las llamadas se ejecutan. Contadores registran fallos, respuestas lentas y opcionalmente errores consecutivos. Un solo 500 de un endpoint flaky no debe abrir el circuito; fallo sostenido sí.

Open — las llamadas fallan rápido sin I/O de red. El caller devuelve fallback o error al instante. Ningún thread bloquea. Ningún reintento dispara contra un peer conocido como enfermo.

Half-open — tras una ventana de sleep, una o pocas requests de probe prueban el dependency. Los éxitos cierran el circuito; los fallos lo reabren y resetean la ventana de sleep.

El diseño de umbrales separa breakers útiles de ruidosos:

failure_rate_threshold = failures / (failures + successes) over rolling window
slow_call_threshold    = responses slower than T ms count as failures
minimum_calls          = do not evaluate until N requests in window (avoid opening on 1/1)
wait_duration_open     = how long to stay open before half-open probes
permitted_half_open    = max concurrent probes in half-open state

Puntos de partida concretos para dependencies HTTP (ajustar con baselines medidos):

Dependency classFailure thresholdSlow thresholdOpen duration
Payment / auth50% over 20 requestsp95 > 2 s30–60 s
Internal gRPC30% over 50 requestsp95 > 500 ms15–30 s
Best-effort enrichment60% over 10 requestsp95 > 1 s10 s

Ventanas deslizantes superan conteos fijos para tráfico en ráfagas. Breakers por dependency superan un breaker global — un breaker global convierte un outage de búsqueda en outage de login.

Instrumentar transiciones de estado: circuit_opened, circuit_half_open, circuit_closed, etiquetados por nombre de dependency y servicio caller. Esos eventos pertenecen al mismo dashboard que SLIs and SLOs without an SRE team — un circuito atascado abierto veinte minutos es un evento de fiabilidad visible al usuario aunque las tasas de error parezcan aceptables porque los fallbacks enmascaran el fallo.

Reintentos y circuit breakers deben coordinarse

Los reintentos multiplican carga. Un circuit breaker existe en parte para detener la multiplicación de reintentos contra un dependency que no puede absorberlos.

Anti-patrón: reintentar tres veces con backoff exponencial, luego envolver todo el loop de reintentos en un breaker que solo ve el fallo final. El dependency recibe tres veces el tráfico mientras el breaker cree que la tasa de fallo es baja porque el cliente absorbió dos éxitos-antes-de-timeout.

Mejor patrón:

  • Aplicar el breaker por intento o contar cada intento fallido hacia la ventana de fallos.
  • Acotar el presupuesto total de reintentos con un contexto de deadline compartido entre intentos.
  • Deshabilitar reintentos cuando el circuito está open — fail fast en el límite del breaker.
  • Usar operaciones idempotentes solo en rutas de reintento; POST no idempotente sin claves de idempotencia no debe reintentar a ciegas.
async function callPaymentProvider(
  ctx: RequestContext,
  payload: ChargeRequest,
): Promise<ChargeResult> {
  if (paymentBreaker.isOpen()) {
    return fallbackQueue.enqueue(payload)
  }
 
  const deadline = Date.now() + 2_000
  let lastError: Error | undefined
 
  for (let attempt = 0; attempt < 2; attempt++) {
    if (Date.now() > deadline) break
 
    try {
      const result = await paymentClient.charge(payload, {
        signal: AbortSignal.timeout(800),
      })
      paymentBreaker.recordSuccess()
      return result
    } catch (err) {
      lastError = err as Error
      paymentBreaker.recordFailure()
      if (paymentBreaker.isOpen()) break
    }
  }
 
  return fallbackQueue.enqueue(payload)
}

Cuando el circuito abre durante checkout, el incidente no es solo "payment API down" — es "pedidos entrando a una cola de reconciliación." Ese comportamiento debe aparecer en incident response runbooks con pasos de verificación: profundidad de cola, edad del mensaje más antiguo, comprobaciones de cargo duplicado cuando el proveedor se recupera.

¿Qué debe incluir una configuración de circuit breaker en producción?

¿Cuándo debe abrir un circuito por latencia en lugar de errores?

Abrir por latencia cuando el dependency devuelve HTTP 200 con lentitud inutilizable — un modo de fallo común en bases sobrecargadas y APIs rate-limited que responden lento en lugar de rechazar. Tratar respuestas más lentas que el deadline del caller como fallos para el breaker aunque el status code sea éxito. Combinar con timeouts upstream para que éxitos lentos nunca retengan recursos más de lo que el SLO visible al usuario permite.

¿Cómo evita el probing half-open el flapping de recuperación?

El flapping ocurre cuando un probe exitoso cierra el circuito, el tráfico vuelve a inundar y el dependency falla de nuevo en segundos. Mitigaciones: exigir múltiples éxitos consecutivos en half-open antes de cerrar; rampar concurrencia permitida gradualmente; usar duración open más larga tras re-apertura que tras la primera apertura. Registrar cada fallo half-open — fallos repetidos suelen indicar que el dependency se recupera en ráfagas, no curado por completo.

¿Los fallbacks deben escribir a la base de datos o solo devolver datos cacheados?

Rutas de lectura: cache stale con header stale-while-revalidate o campo de respuesta suele ser seguro cuando el producto tolera minutos de lag. Rutas de escritura: preferir enqueue durable sobre drop silencioso. Un fallback que descarta un payment intent es peor que un pago lento. Documentar si la ruta fallback es lossy, at-least-once o exactly-once y probar recuperación en consecuencia.

A common argument runs the other way

Un argumento frecuente sostiene que los circuit breakers ocultan problemas de dependencies, fomentan fallbacks perezosos y añaden complejidad operacional mejor invertida en arreglar causas raíz — que timeouts y buenos SLOs en vendors upstream son suficientes.

Los timeouts solos no detienen tormentas de reintentos de cientos de instancias. Los SLOs de vendor no evitan que un blip regional de DNS sature pools de conexión antes de que alguien lea la status page. Los circuit breakers no reemplazan arreglar dependencies; acotan cuánto tiempo la aplicación participa en fallo en cascada mientras ocurre la reparación. Equipos que nunca abren un circuito o tienen umbrales demasiado laxos o no instrumentan transiciones de estado — ambos son gaps de configuración, no evidencia de que el patrón falló.

Key takeaways

  • El aislamiento de fallos con circuit breaker convierte enfermedad de dependency de starvation de threads en fast-fail con fallback definido.
  • Breakers por dependency, ventanas deslizantes y conteos mínimos de llamadas evitan abrir por ruido.
  • Los reintentos deben contar hacia tasas de fallo del breaker; nunca reintentar contra un circuito open.
  • Los probes half-open necesitan reglas de éxito consecutivo para evitar flapping de recuperación.
  • Instrumentar eventos open, half-open y close junto a SLIs — fallbacks enmascarados aún consumen error budget.
  • Los runbooks deben cubrir rutas fallback: profundidad de cola, reconciliación y comprobaciones de operaciones duplicadas.

Conclusion

Los dependencies fallan en cámara lenta más a menudo que con un connection refused limpio. La flota paga esa lentitud cuando cada servicio reintenta indefinidamente contra el mismo endpoint saturado. Los circuit breakers son la capa de admission control para confianza saliente: umbrales explícitos, fallbacks explícitos, probes de recuperación explícitos.

La próxima integración de dependency debería salir con configuración de breaker revisada en el mismo PR que el client wrapper — no añadida tras el primer outage que llene el postmortem con "retry storm." El aislamiento de fallos es una decisión de diseño. Es más barato antes de que el tráfico de producción demuestre que el default es ningún aislamiento.

Artículos relacionados

Paleta de comandos

Buscá un comando para ejecutar...