Circuit Breaker

Cómo proteger tus servicios de fallos en cascada usando el patrón Circuit Breaker con estados, fallbacks y recuperación automática.

Qué problema resuelve

En una arquitectura de microservicios, los servicios dependen unos de otros. Cuando un servicio downstream se vuelve lento o deja de responder, los servicios que lo llaman acumulan conexiones abiertas, agotan sus thread pools y eventualmente también fallan. Este efecto dominó se conoce como fallo en cascada.

Ejemplo del problema

Usuario → API Gateway → Servicio A → Servicio B (lento/caído)
                                         ⏳ timeout 30s
                                         ⏳ timeout 30s
                                         ⏳ timeout 30s

              Servicio A agota sus threads
              esperando respuesta de B

              API Gateway agota sus threads
              esperando respuesta de A

              Todo el sistema deja de responder

Sin protección, un solo servicio lento puede tumbar toda la plataforma. El Circuit Breaker actúa como un interruptor automático que corta la comunicación con un servicio que está fallando, evitando que el problema se propague.

El patrón fue popularizado por Michael Nygard en su libro Release It! y toma su nombre de los interruptores eléctricos que cortan el circuito cuando detectan una sobrecarga.

Impacto real de los fallos en cascada

MétricaSin Circuit BreakerCon Circuit Breaker
Tiempo de detecciónMinutos (cuando los usuarios reportan)Segundos (cambio de estado del circuito)
Servicios afectadosTodos los que dependen del servicio caídoSolo el servicio caído
RecuperaciónManual (reiniciar servicios, limpiar colas)Automática (semi-abierto detecta recuperación)
Experiencia de usuarioTimeouts largos, errores 500Respuesta rápida con fallback o error claro
Recursos consumidosThreads y conexiones agotadosRecursos liberados inmediatamente

Cómo funciona

El Circuit Breaker es un proxy que se coloca entre un servicio y sus dependencias. Monitorea las llamadas y, basándose en la tasa de fallos, decide si permite o bloquea las solicitudes.

Los tres estados

                    Fallos > umbral
    ┌──────────┐ ──────────────────► ┌──────────┐
    │ CERRADO  │                     │ ABIERTO  │
    │ (normal) │ ◄──────────────── │ (cortado)│
    └──────────┘   Prueba exitosa    └────┬─────┘
         ▲                                │
         │         Timeout expira         │
         │                                ▼
         │                          ┌───────────┐
         └────── Prueba exitosa ────│SEMI-ABIERTO│
                                    └───────────┘
                  Prueba falla ────► ABIERTO

Estado Cerrado (Closed)

Es el estado normal. Todas las solicitudes pasan al servicio downstream. El circuit breaker monitorea los resultados:

  • Cuenta los fallos recientes (errores, timeouts)
  • Si la tasa de fallos supera un umbral (ej: 50% de fallos en las últimas 10 llamadas), cambia a Abierto

Estado Abierto (Open)

El circuito está cortado. Las solicitudes no llegan al servicio downstream. En su lugar, el circuit breaker responde inmediatamente con un error o ejecuta un fallback.

  • Evita enviar tráfico a un servicio que sabemos que está fallando
  • Libera recursos (threads, conexiones) que se desperdiciarían esperando timeouts
  • Después de un período de espera (ej: 30 segundos), cambia a Semi-Abierto

Estado Semi-Abierto (Half-Open)

Estado de prueba. El circuit breaker permite pasar un número limitado de solicitudes para verificar si el servicio se recuperó:

  • Si las solicitudes de prueba son exitosas → vuelve a Cerrado
  • Si las solicitudes de prueba fallan → vuelve a Abierto

Parámetros de configuración

ParámetroDescripciónEjemploConsideración
Failure thresholdPorcentaje de fallos para abrir50%Muy bajo = falsos positivos, muy alto = reacción lenta
Request volumeMínimo de solicitudes antes de evaluar10 solicitudesEvita abrir el circuito por 1 fallo aislado
Sleep windowTiempo en estado abierto30 segundosDebe dar tiempo al servicio para recuperarse
Success thresholdÉxitos para cerrar el circuito3 consecutivasMás alto = más confianza en la recuperación
TimeoutTiempo máximo de espera5 segundosDebe ser menor que el timeout del cliente

Estrategias de fallback

Cuando el circuito está abierto, en lugar de devolver un error crudo, puedes implementar fallbacks:

EstrategiaDescripciónEjemplo
Valor por defectoDevolver datos estáticosMostrar catálogo cacheado
CachéÚltima respuesta exitosa cacheadaPrecio del producto de hace 5 min
Servicio alternativoRedirigir a un respaldoUsar servicio de pagos secundario
Degradación eleganteFuncionalidad reducidaMostrar “precio no disponible”
Fail fastError inmediato con mensaje claroHTTP 503 con retry-after header

Ejemplo de flujo completo

Estado: CERRADO
  Llamada 1 → Servicio B → ✅ OK
  Llamada 2 → Servicio B → ✅ OK
  Llamada 3 → Servicio B → ❌ Timeout
  Llamada 4 → Servicio B → ❌ Error 500
  Llamada 5 → Servicio B → ❌ Timeout
  → Tasa de fallos: 60% > umbral 50%
  → Cambio a ABIERTO ⚡

Estado: ABIERTO
  Llamada 6 → ⚡ Fallback inmediato (no llega a B)
  Llamada 7 → ⚡ Fallback inmediato
  ... (30 segundos de espera)
  → Cambio a SEMI-ABIERTO

Estado: SEMI-ABIERTO
  Llamada 8 → Servicio B → ✅ OK (prueba)
  Llamada 9 → Servicio B → ✅ OK (prueba)
  Llamada 10 → Servicio B → ✅ OK (prueba)
  → 3 éxitos consecutivos
  → Cambio a CERRADO ✅

Ejemplo práctico: Circuit Breaker en un servicio de pagos

Imagina un servicio de pedidos que llama a un proveedor externo de pagos:

// Pseudocódigo de configuración del Circuit Breaker
circuitBreaker = new CircuitBreaker({
  name: "payment-provider",
  failureThreshold: 50,        // 50% de fallos
  requestVolume: 10,           // mínimo 10 requests para evaluar
  sleepWindow: 30000,          // 30 segundos en estado abierto
  successThreshold: 3,         // 3 éxitos para cerrar
  timeout: 5000,               // 5 segundos de timeout por llamada
  fallback: () => ({
    status: "PENDING",
    message: "Pago en cola, se procesará cuando el servicio se recupere"
  })
});

// Uso
async function procesarPago(pedidoId, monto) {
  try {
    resultado = await circuitBreaker.execute(() =>
      paymentProvider.charge(pedidoId, monto)
    );
    return resultado;
  } catch (error) {
    if (error instanceof CircuitOpenError) {
      // El circuito está abierto, se ejecutó el fallback
      encolarPagoParaReintento(pedidoId, monto);
      return error.fallbackResult;
    }
    throw error;
  }
}

Monitoreo del Circuit Breaker

Los cambios de estado del circuito son señales valiosas para el equipo de operaciones:

// Métricas a exponer
circuit_breaker_state{name="payment-provider"} = "closed" | "open" | "half_open"
circuit_breaker_calls_total{name="payment-provider", result="success"}
circuit_breaker_calls_total{name="payment-provider", result="failure"}
circuit_breaker_calls_total{name="payment-provider", result="rejected"}
circuit_breaker_state_transitions_total{name="payment-provider", from="closed", to="open"}

// Alertas recomendadas
ALERT CircuitBreakerOpen
  IF circuit_breaker_state == "open"
  FOR 1m
  LABELS { severity = "warning" }
  ANNOTATIONS { summary = "Circuit breaker abierto para payment-provider" }

Ventajas

  • Previene fallos en cascada: Un servicio caído no arrastra al resto del sistema
  • Fail fast: Las solicitudes fallan inmediatamente en lugar de esperar timeouts largos
  • Liberación de recursos: No se desperdician threads ni conexiones en llamadas que van a fallar
  • Recuperación automática: El estado semi-abierto detecta cuándo el servicio se recupera
  • Experiencia de usuario mejorada: Los fallbacks ofrecen respuestas degradadas en lugar de errores
  • Observabilidad: Los cambios de estado son señales claras para alertas y monitoreo

Trade-offs / Desventajas

  • Configuración delicada: Umbrales incorrectos pueden abrir el circuito demasiado pronto o tarde
  • Complejidad en fallbacks: Diseñar fallbacks útiles para cada dependencia requiere esfuerzo
  • Datos desactualizados: Los fallbacks basados en caché pueden devolver información obsoleta
  • Testing complejo: Simular los tres estados y las transiciones requiere infraestructura
  • Latencia en recuperación: El sleep window introduce un retraso entre la recuperación real y la detección
  • No resuelve la causa raíz: El circuit breaker protege al sistema, pero el servicio downstream sigue necesitando arreglo

Cuándo usar

  • Llamadas a servicios externos o microservicios que pueden fallar o volverse lentos
  • Sistemas donde un fallo en una dependencia no debe afectar la disponibilidad general
  • Cuando necesitas proteger recursos compartidos (thread pools, connection pools)
  • APIs que llaman a servicios de terceros con SLAs variables
  • Sistemas con requisitos de alta disponibilidad donde la degradación elegante es preferible
  • Cuando ya tienes métricas y alertas para monitorear el estado de los circuitos

Cuándo evitar

  • Llamadas a recursos locales (base de datos local, archivos) donde el overhead no se justifica
  • Operaciones que deben completarse sí o sí (sin alternativa de fallback posible)
  • Sistemas simples con pocas dependencias externas
  • Cuando la latencia adicional del proxy es inaceptable
  • Llamadas asíncronas donde ya tienes colas con reintentos y dead letter queues
  • Prototipos o MVPs donde la resiliencia no es prioridad

Tecnologías e implementaciones comunes

CategoríaOpciones
LibreríasResilience4j (Java), Polly (.NET), Hystrix (Java, legacy), opossum (Node.js)
Service MeshIstio, Linkerd, Envoy (circuit breaking a nivel de infraestructura)
CloudAWS App Mesh, Azure Traffic Manager
MonitoreoPrometheus + Grafana, Datadog, New Relic
Patrones complementariosRetry con backoff, Bulkhead, Timeout, Rate Limiting

Relación con otros patrones

El Circuit Breaker rara vez se usa solo. Funciona mejor en combinación con otros patrones de resiliencia:

┌─────────────────────────────────────────┐
│           Cadena de resiliencia         │
│                                         │
│  Solicitud                              │
│     │                                   │
│     ▼                                   │
│  ┌──────────┐                           │
│  │ Timeout  │  Limita tiempo de espera  │
│  └────┬─────┘                           │
│       ▼                                 │
│  ┌──────────┐                           │
│  │  Retry   │  Reintenta con backoff    │
│  └────┬─────┘                           │
│       ▼                                 │
│  ┌──────────┐                           │
│  │ Circuit  │  Corta si hay muchos      │
│  │ Breaker  │  fallos consecutivos      │
│  └────┬─────┘                           │
│       ▼                                 │
│  ┌──────────┐                           │
│  │ Bulkhead │  Aísla recursos por       │
│  │          │  dependencia              │
│  └────┬─────┘                           │
│       ▼                                 │
│  Servicio downstream                    │
└─────────────────────────────────────────┘
  • Timeout Pattern: Define cuánto esperar antes de considerar una llamada como fallida. El timeout alimenta al circuit breaker con datos de fallos
  • Retry Pattern: Reintenta llamadas fallidas con backoff exponencial. Los reintentos ocurren antes de que el circuito se abra; una vez abierto, no se reintenta
  • Bulkhead: Aísla los recursos (threads, conexiones) por dependencia para que un servicio lento no consuma todos los recursos del sistema
  • Saga Pattern: Cuando un paso de la saga falla y el circuit breaker está abierto, la saga activa compensaciones en lugar de reintentar indefinidamente
  • API Gateway: El gateway puede implementar circuit breakers para cada servicio backend, centralizando la protección
  • Idempotency Pattern: Cuando el circuito se cierra y se reintentan operaciones, la idempotencia garantiza que no se dupliquen efectos

Errores comunes

ErrorConsecuenciaSolución
Umbral demasiado bajoCircuito se abre por fallos aisladosAumentar request volume mínimo
Sleep window muy cortoCircuito oscila entre abierto y cerradoDar más tiempo de recuperación
Sin fallback definidoError crudo al usuario cuando el circuito abreDiseñar fallbacks para cada dependencia
Un circuit breaker globalUn servicio lento afecta a todosUn circuit breaker por dependencia
No monitorear transicionesNo detectas problemas a tiempoAlertas por cambios de estado
Ignorar el estado semi-abiertoTráfico excesivo en pruebaLimitar requests de prueba

Próximos pasos

El Circuit Breaker es una pieza fundamental de la resiliencia en microservicios. Para completar tu comprensión de patrones de resiliencia, explora el Retry Pattern para manejar fallos transitorios con backoff, el Timeout Pattern para limitar esperas, y el Bulkhead Pattern para aislar recursos por dependencia.