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étrica | Sin Circuit Breaker | Con Circuit Breaker |
|---|---|---|
| Tiempo de detección | Minutos (cuando los usuarios reportan) | Segundos (cambio de estado del circuito) |
| Servicios afectados | Todos los que dependen del servicio caído | Solo el servicio caído |
| Recuperación | Manual (reiniciar servicios, limpiar colas) | Automática (semi-abierto detecta recuperación) |
| Experiencia de usuario | Timeouts largos, errores 500 | Respuesta rápida con fallback o error claro |
| Recursos consumidos | Threads y conexiones agotados | Recursos 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ámetro | Descripción | Ejemplo | Consideración |
|---|---|---|---|
| Failure threshold | Porcentaje de fallos para abrir | 50% | Muy bajo = falsos positivos, muy alto = reacción lenta |
| Request volume | Mínimo de solicitudes antes de evaluar | 10 solicitudes | Evita abrir el circuito por 1 fallo aislado |
| Sleep window | Tiempo en estado abierto | 30 segundos | Debe dar tiempo al servicio para recuperarse |
| Success threshold | Éxitos para cerrar el circuito | 3 consecutivas | Más alto = más confianza en la recuperación |
| Timeout | Tiempo máximo de espera | 5 segundos | Debe 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:
| Estrategia | Descripción | Ejemplo |
|---|---|---|
| Valor por defecto | Devolver datos estáticos | Mostrar catálogo cacheado |
| Caché | Última respuesta exitosa cacheada | Precio del producto de hace 5 min |
| Servicio alternativo | Redirigir a un respaldo | Usar servicio de pagos secundario |
| Degradación elegante | Funcionalidad reducida | Mostrar “precio no disponible” |
| Fail fast | Error inmediato con mensaje claro | HTTP 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ía | Opciones |
|---|---|
| Librerías | Resilience4j (Java), Polly (.NET), Hystrix (Java, legacy), opossum (Node.js) |
| Service Mesh | Istio, Linkerd, Envoy (circuit breaking a nivel de infraestructura) |
| Cloud | AWS App Mesh, Azure Traffic Manager |
| Monitoreo | Prometheus + Grafana, Datadog, New Relic |
| Patrones complementarios | Retry 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
| Error | Consecuencia | Solución |
|---|---|---|
| Umbral demasiado bajo | Circuito se abre por fallos aislados | Aumentar request volume mínimo |
| Sleep window muy corto | Circuito oscila entre abierto y cerrado | Dar más tiempo de recuperación |
| Sin fallback definido | Error crudo al usuario cuando el circuito abre | Diseñar fallbacks para cada dependencia |
| Un circuit breaker global | Un servicio lento afecta a todos | Un circuit breaker por dependencia |
| No monitorear transiciones | No detectas problemas a tiempo | Alertas por cambios de estado |
| Ignorar el estado semi-abierto | Tráfico excesivo en prueba | Limitar 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.