Módulo 5: Circuit breakers
1. Presentación del módulo: deja de golpear a un muerto
Descripción
En los módulos 2, 3 y 4 aprendiste a sobrevivir a una dependencia que hipa. Le pusiste un timeout a shipping para que un cuelgue no te agotara los hilos; le pusiste backoff y jitter al reintento de payments para no matarlo con una avalancha; le pusiste una idempotency_key para que ese reintento no cobrara dos veces. Las tres herramientas comparten un supuesto que casi nunca se dice en voz alta: que la dependencia va a volver pronto. El timeout corta y liberas el hilo confiando en que el próximo intento será mejor. El retry insiste precisamente porque apuesta a que el problema es de un segundo. Pero, ¿y si no es de un segundo? ¿Y si shipping no hipó, sino que se murió —lleva treinta segundos devolviendo el mismo error, o colgándose hasta el timeout, en cada una de las llamadas—?
En ese mundo, todo lo que aprendiste sigue "funcionando" y sin embargo estás perdiendo. Cada llamada a shipping espera el timeout completo antes de fallar: dos segundos de un hilo de orders ocupado esperando una respuesta que no va a llegar. El retry, fiel a su naturaleza, vuelve a intentar —y vuelve a esperar dos segundos, y vuelve a fallar—. La idempotencia garantiza que no haces daño, pero no evita que estés gastando: hilos ocupados, latencia acumulada, capacidad de orders quemada en confirmar, una y otra vez, algo que ya sabías desde el segundo fallo. Estás golpeando a un muerto. Y peor: cada golpe es tráfico que le cae encima al servicio caído justo cuando intenta levantarse. No solo desperdicias tus recursos; le estorbas la recuperación a él.
Este módulo trata del patrón que reconoce ese estado y hace lo único sensato: deja de llamar. Se llama circuit breaker —interruptor de circuito—, y su mecánica es una máquina de tres estados. En CLOSED (cerrado, el circuito conduce) todo el tráfico pasa normal y el breaker se limita a contar los fallos. Cuando los fallos consecutivos llegan a un umbral, el breaker abre el circuito (OPEN): de ahí en adelante rechaza toda llamada de inmediato, sin siquiera intentar contactar a shipping. Falla rápido en lugar de fallar lento. Y tras un tiempo de espera —el cooldown—, entra en HALF_OPEN (medio abierto): deja pasar una llamada de prueba. Si esa prueba tiene éxito, el servicio revivió y el breaker vuelve a CLOSED; si falla, sigue muerto y el breaker vuelve a OPEN a esperar otro rato. Se recupera solo, sin que nadie apriete un botón. Eso es todo el módulo, y lo vas a construir y medir: sobre orders→shipping de Mercado, con shipping muriendo un rato y reviviendo después.
Conexión con el módulo: esta es la lección-mapa. No construimos el breaker completo todavía; hoy fijamos la intuición (por qué seguir llamando a un muerto es un problema distinto del de una dependencia lenta), la metáfora (el interruptor eléctrico de tu casa, de donde Nygard tomó el nombre), el vocabulario (los tres estados, failure_threshold, cooldown, CircuitOpenError) y el escenario medido que recorreremos. La lección 2 mide el "antes": el costo real de seguir llamando a shipping muerto sin breaker. La 3 construye la máquina de tres estados entera. La 4 mide la transición CLOSED → OPEN (abrir por umbral). La 5 mide HALF_OPEN y la recuperación automática. La 6 aclara por qué el breaker es distinto del retry —y por qué se combinan—. La 7 te enseña a elegir el umbral y el cooldown con datos. Y la 8 te pone a blindar shipping con tus manos y a medir el antes y el después.
La analogía: el interruptor que salta antes de que se incendie la casa
La palabra "circuit breaker" no es una metáfora vaga: es el aparato literal que tienes en el tablero eléctrico de tu casa. Michael Nygard, que le puso ese nombre al patrón en Release It!, eligió la imagen con cuidado, y vale la pena entenderla bien porque explica cada decisión del diseño.
Imagina el cableado de tu casa sin interruptor. Un electrodoméstico se descompone y hace un cortocircuito: la corriente, que debería encontrar resistencia, de repente fluye sin freno. El cable se calienta, se calienta más, y en unos minutos el aislante se derrite y la pared empieza a arder. El problema fue del electrodoméstico, pero el que se incendia es todo lo demás —la casa entera paga la falla de un aparato—. Esa es la falla en cascada del módulo 1, en versión eléctrica: la falla de un componente se propaga por el medio que los conecta (el cable) y tumba al sistema completo.
El interruptor de circuito es un aparatito que se mete en medio de ese cable con una sola misión: cuando detecta que pasa demasiada corriente, salta y corta el paso. En cuanto salta, el cable deja de calentarse —no llega corriente que lo caliente— y la casa se salva. Fíjate en lo que no hace el interruptor: no arregla el electrodoméstico descompuesto (ese sigue roto), y no adivina cuándo lo vas a reparar. Solo hace una cosa, rápido: corta, para que la falla de un aparato no se coma la casa. Y cuando tú arreglas el problema, subes la palanca a mano y la corriente vuelve.
El circuit breaker de software es exactamente eso, con orders como la casa, shipping como el electrodoméstico descompuesto, y las llamadas como la corriente. Cuando shipping empieza a fallar en cadena, el breaker salta (OPEN) y corta las llamadas —para que la falla de shipping no le agote los hilos a orders ni tumbe el checkout entero—. Deja de conducir a propósito. La única diferencia con el interruptor de tu casa es el final feliz: el de software no necesita que subas la palanca a mano. Cada tanto se pregunta solo "¿ya lo arreglaron?" —eso es HALF_OPEN, la llamada de prueba— y si la respuesta es sí, vuelve a conducir solito. Un interruptor eléctrico que, además de saltar para salvarte la casa, revisa cada rato si ya puede volver a dar luz.
Guarda esta imagen, porque desarma de golpe la objeción más común contra el patrón: "¿cómo va a ser bueno dejar de llamar a un servicio?". Es exactamente igual de bueno que dejar de mandar corriente por un cable en cortocircuito. No estás abandonando a shipping; estás evitando que su falla queme a orders, y de paso le quitas de encima el tráfico que le impedía recuperarse.
Los tres estados de un vistazo
Antes de medir nada, quédate con la máquina de estados. Son tres posiciones y las transiciones entre ellas; el módulo entero es aprender cada flecha con su número.
stateDiagram-v2
[*] --> CLOSED
CLOSED --> OPEN: failure_count llega a failure_threshold
OPEN --> HALF_OPEN: pasa el cooldown
HALF_OPEN --> CLOSED: la prueba tiene exito (el servicio revivio)
HALF_OPEN --> OPEN: la prueba falla (sigue muerto)
note right of CLOSED
Pasa todo el trafico.
Cuenta los fallos consecutivos.
Un exito resetea el contador.
end note
note right of OPEN
Rechaza toda llamada al instante
(CircuitOpenError, ~0 ms).
No toca la dependencia.
end note
note right of HALF_OPEN
Deja pasar UNA llamada de prueba.
Segun el resultado: CLOSED u OPEN.
end note
CLOSED es el estado normal, el de todos los días. El circuito conduce: cada llamada de orders pasa a shipping como si el breaker no existiera. Lo único que hace el breaker aquí es mirar: lleva la cuenta de cuántos fallos consecutivos van (failure_count). Un éxito pone el contador en cero —un fallo aislado no significa nada—. Pero si los fallos se encadenan y llegan al umbral (failure_threshold), el breaker concluye "esto no es un hipo, está muerto" y salta a OPEN.
OPEN es el estado de protección. El circuito está cortado: ninguna llamada llega a shipping. En cuanto orders intenta llamar, el breaker rechaza al instante lanzando un CircuitOpenError —en microsegundos, no en los dos segundos del timeout—. Aquí es donde el breaker gana: no gasta hilos, no gasta latencia, y no le manda tráfico a shipping para que se recupere en paz. El breaker anota cuándo abrió (opened_at) para saber cuánto lleva esperando.
HALF_OPEN es el estado de prueba, y el que vuelve al breaker inteligente. Cuando ha pasado el cooldown desde que abrió, el breaker no reabre el diluvio de golpe: deja pasar una sola llamada, a modo de sonda. Si esa llamada tiene éxito, interpreta "revivió" y vuelve a CLOSED (circuito normal). Si falla, interpreta "sigue muerto", vuelve a OPEN, y reinicia el reloj del cooldown para probar de nuevo más tarde. Es el "¿ya lo arreglaron?" del interruptor que se revisa solo.
El primer número: 45 llamadas que nunca tocaron al muerto
Para que la máquina de estados no se quede en abstracto, adelanto el número que vas a medir en detalle a lo largo del módulo. El escenario, con la semilla fija de toda la guía: orders llama a shipping una vez por segundo durante 90 segundos. shipping se cae por completo del segundo 10 al 60 (50 segundos de apagón) y revive en el 60. Cada llamada a shipping caído se cuelga y la corta un timeout de 2 segundos. Comparamos orders sin breaker contra orders con un breaker de failure_threshold=5 y cooldown=10s.
Qué esperar. Esta es la salida real de la simulación (Python, semilla fija, determinista):
MERCADO · orders -> shipping (shipping DOWN durante [10s, 60s))
timeout=2.0s healthy=20ms 1 llamada/s por 90s
(a) SIN breaker
llamadas que esperaron el timeout : 50
hilos-segundo quemados esperando : 100.8s
(b) CON breaker (failure_threshold=5, cooldown=10s)
llamadas que REALMENTE tocaron shipping : 9
llamadas RECHAZADAS en ~0 ms (ahorradas): 45
hilos-segundo quemados esperando : 18.7s
>> llamadas ahorradas de tocar un servicio muerto: 45
>> esperas de timeout evitadas: 41 (82.1 hilos-segundo no quemados)
Léelo despacio, porque es la tesis del módulo en cifras. Sin breaker, las 50 llamadas que caen durante el apagón esperan cada una el timeout completo: 50 × 2 s = 100.8 hilos-segundo de orders quemados esperando respuestas que nunca llegan (los 0.8 extra son las llamadas sanas de 20 ms). Cada uno de esos hilos, mientras espera, no atiende a nadie más. Con breaker, en cambio, solo 9 llamadas tocan de verdad a shipping (las 5 que abren el circuito, más 4 sondas de HALF_OPEN que fallan mientras sigue muerto). Las otras 45 se rechazan en ~0 ms, sin tocar a shipping, sin quemar un hilo, sin estorbarle la recuperación. El breaker convirtió 100.8 hilos-segundo de espera inútil en 18.7, y salvó 45 llamadas de golpear a un servicio que no iba a responder.
Y hay un cierre que la tabla no muestra pero la traza sí, y que veremos en la lección 5: cuando shipping revive en el segundo 60, el breaker se da cuenta solo cuatro segundos después (una sonda de HALF_OPEN tiene éxito en t=64) y vuelve a CLOSED. Nadie tocó nada. El circuito abrió para proteger, probó cada tanto, y cerró cuando el servicio volvió. Ese ciclo completo —CLOSED → OPEN → HALF_OPEN → CLOSED— es lo que construyes en este módulo.
Por qué esto no lo resuelve lo que ya sabes
Una reacción natural es "pero yo ya tengo timeout y retry, ¿no basta con eso?". Vale la pena cerrar esa puerta ahora, porque marca la frontera exacta de por qué el breaker es una herramienta nueva y no un lujo redundante.
El timeout (módulo 2) hace que cada llamada a shipping colgado falle en 2 segundos en vez de en el infinito. Es indispensable —sin él, el breaker ni se enteraría de que algo va mal—, pero no evita que sigas pagando esos 2 segundos en cada llamada. El timeout acota el costo de un intento; no evita que hagas mil intentos igual de costosos contra un servicio que no va a responder. El breaker ataca justo eso: elimina los intentos.
El retry (módulo 3) es, si acaso, contraproducente aquí. Su instinto es insistir, porque está diseñado para el fallo transitorio: "falló, quizás el próximo salga". Contra un shipping muerto durante 50 segundos, ese instinto multiplica el castigo —más llamadas, más timeouts, más tráfico sobre el servicio caído—. El retry insiste; el breaker se rinde. Son respuestas opuestas al mismo síntoma, apropiadas para causas distintas (una falla de milisegundos vs. una de minutos), y la lección 6 las enfrenta cara a cara.
La idempotencia (módulo 4) garantiza que reintentar no corrompe el estado, pero no dice nada sobre el desperdicio. Puedes golpear a un muerto de forma perfectamente idempotente —sin cobrar dos veces, sin duplicar nada— y aun así estar quemando 100 hilos-segundo en confirmar lo obvio. La idempotencia hace seguro el reintento; el breaker decide cuándo dejar de reintentar del todo.
La conclusión es que el breaker cubre un caso que las otras tres herramientas dejan abierto: el fallo persistente. Timeout, retry e idempotencia asumen que la dependencia vuelve pronto y optimizan el intento individual. El breaker reconoce que la dependencia no va a volver pronto y optimiza la decisión de más alto nivel: dejar de intentar. Por eso no se sustituyen —se apilan—, y por eso el capstone (módulo 8) los combina todos en una sola llamada blindada.
Errores comunes
Creer que el breaker "abandona" a la dependencia. Qué pasa: alguien oye "el breaker deja de llamar a shipping" y lo interpreta como rendirse, como abandonar al servicio. Por qué pasa: "dejar de intentar" suena a derrota. Cómo detectarlo: si tu instinto es "pero hay que seguir intentando para que se recupere", tienes la causalidad al revés. Cómo corregirlo: recuerda el interruptor eléctrico —cortar la corriente al cable en cortocircuito no abandona al cable, lo salva de incendiarse, y salva a la casa entera—. Dejar de llamar a shipping muerto no lo abandona: le quita de encima el tráfico que le impedía recuperarse, y protege a orders de agotarse. El breaker ayuda a las dos partes; insistir las perjudica a las dos.
Confundir "dependencia lenta" con "dependencia muerta". Qué pasa: se aplica (o no) el breaker sin distinguir si el problema es latencia o caída. Por qué pasa: desde orders, ambos síntomas se parecen —la llamada no responde a tiempo—. Cómo detectarlo: pregúntate cuánto lleva fallando. Un pico de latencia de un segundo es trabajo del timeout y quizás del retry; 30 segundos de fallos en cadena es trabajo del breaker. Cómo corregirlo: el breaker no reemplaza al timeout ni al retry; se suma para el caso que ellos no cubren (el fallo sostenido). El módulo entero es aprender a leer la diferencia, y el failure_threshold es justamente el número que traza la línea entre "hipo" y "muerte".
Pensar que el breaker arregla la dependencia. Qué pasa: se pone un breaker sobre shipping y se cierra el ticket del incidente como si el problema estuviera resuelto. Por qué pasa: el checkout deja de agotarse, así que "parece" arreglado. Cómo detectarlo: shipping sigue caído; lo único que cambió es que orders ya no se cae con él. Cómo corregirlo: el breaker es contención de daño, no reparación —igual que el interruptor no repara el electrodoméstico—. Que orders sobreviva no significa que los envíos se estén creando; para eso hace falta o que shipping se arregle, o que orders degrade (difiera el envío, módulo 7). El breaker te compra tiempo y protege al resto; no cura la causa.
Ejercicios
Ejercicio 1 — Traduce la metáfora. Completa la correspondencia entre el interruptor eléctrico de tu casa y el circuit breaker de software. Para cada elemento eléctrico, di cuál es su equivalente en orders→shipping: (a) la corriente que fluye por el cable; (b) el electrodoméstico que hace cortocircuito; (c) el momento en que el interruptor salta; (d) subir la palanca a mano para restaurar la luz. ¿Cuál de los cuatro es distinto en el breaker de software, y por qué es una mejora?
Ver solución
- (a) La corriente = las llamadas de
ordersashipping. Es lo que fluye por el "cable" (la red que los conecta) y lo que el breaker deja pasar o corta. - (b) El electrodoméstico en cortocircuito =
shippingcaído. Es el componente descompuesto cuya falla amenaza con propagarse. - (c) El interruptor salta = la transición
CLOSED → OPEN. El breaker detecta demasiados fallos (el equivalente a demasiada corriente) y corta el paso para proteger al resto. - (d) Subir la palanca a mano = el equivalente sería un reset manual del breaker. Este es el que es distinto, y es la mejora: el breaker de software no necesita que un humano suba la palanca. Entra solo en
HALF_OPENtras elcooldown, prueba con una llamada, y si el servicio revivió se cierra automáticamente (HALF_OPEN → CLOSED). La recuperación es autónoma. Un interruptor eléctrico que se revisara solo cada tanto y volviera a dar luz cuando el problema desapareciera sería exactamente el circuit breaker de software —esa autonomía es lo que lo hace apto para un sistema que debe recuperarse sin que nadie esté de guardia a las 3 a.m.
Ejercicio 2 — Lee el número. Usando la tabla del "primer número": (a) ¿cuántas llamadas caen durante el apagón de 50 s, y por qué sin breaker las 50 esperan el timeout completo? (b) Con breaker solo 9 llamadas tocan a shipping; ¿de dónde salen esas 9 y por qué no son ni 0 ni 50? (c) Explica en una frase qué significan los "82.1 hilos-segundo no quemados" para orders.
Ver solución
(a) orders llama una vez por segundo y el apagón va del segundo 10 al 60, así que caen 50 llamadas dentro del apagón (una por cada segundo de los 50). Sin breaker, orders no tiene memoria de que shipping está muerto: cada llamada lo intenta de nuevo, se cuelga, y la corta el timeout a los 2 segundos. 50 llamadas × 2 s = 100 hilos-segundo de espera inútil (más las sanas de 20 ms redondean a 100.8).
(b) Las 9 salen de dos fuentes. Primero, las 5 llamadas que abren el circuito: el breaker está en CLOSED y necesita ver failure_threshold=5 fallos consecutivos antes de saltar a OPEN, así que las 5 primeras llamadas del apagón sí tocan a shipping y fallan (esas cuentan hacia el umbral). Segundo, las 4 sondas de HALF_OPEN: cada cooldown de 10 s el breaker deja pasar una llamada de prueba para ver si shipping revivió; como sigue muerto, esas 4 sondas (en t=24, 34, 44, 54) fallan y reabren el circuito. 5 + 4 = 9. No son 0 porque el breaker necesita tocar al servicio para descubrir que está muerto (al principio) y para descubrir que revivió (las sondas); no son 50 porque entre medias rechaza todo sin tocar nada.
(c) Significa que orders recuperó 82.1 segundos de tiempo de hilo que, sin breaker, habría pasado congelado esperando timeouts —tiempo que ahora sus hilos pueden dedicar a atender el tráfico sano que no depende de shipping—.
Ejercicio 3 — ¿Qué falla resuelve? Un compañero dice: "ya tenemos timeout y retry con backoff en la llamada a shipping; el breaker es redundante". Da dos escenarios concretos: uno donde tiene razón (el breaker aporta poco) y uno donde se equivoca (el breaker es indispensable). Nombra la variable que distingue los dos escenarios.
Ver solución
Donde tiene (casi) razón —fallo transitorio y raro: shipping tiene un hipo de 300 ms una vez cada tanto, aislado, y luego responde normal. Aquí el timeout + retry con backoff hacen casi todo el trabajo: el retry reintenta, el segundo intento funciona, y el usuario ni se entera. El breaker aporta poco porque nunca llega a acumular failure_threshold fallos consecutivos —un éxito resetea el contador antes de que abra—. Para hipos aislados, el retry es la herramienta y el breaker duerme.
Donde se equivoca —fallo persistente: shipping se cae por completo 50 segundos (un despliegue malo, una caída de su base de datos). Ahora el retry es contraproducente: insiste contra un muerto, y cada intento cuesta 2 segundos de timeout más el tráfico que le echa encima a shipping. Sin breaker, orders quema 100.8 hilos-segundo y le estorba la recuperación a shipping. Con breaker, tras 5 fallos corta y ahorra 45 llamadas. Aquí el breaker es indispensable y el retry, sin el breaker que lo detenga, es parte del problema.
La variable que los distingue es la duración/persistencia del fallo. Fallo corto y aislado → dominio del retry. Fallo largo y sostenido → dominio del breaker. Por eso no son redundantes: cubren extremos opuestos del espectro de "cuánto dura la falla", y en un sistema real conviven (lección 6).
Resumen y siguiente paso
En esta lección viste el problema que el breaker resuelve —seguir golpeando a una dependencia muerta— y por qué es distinto de todo lo que ya sabes: el timeout acota el intento pero no lo elimina, el retry insiste cuando debería rendirse, la idempotencia hace seguro el golpe pero no lo evita. Viste la metáfora que le da nombre (el interruptor que salta para que la falla de un aparato no incendie la casa) y la máquina de tres estados (CLOSED cuenta, OPEN corta, HALF_OPEN prueba). Y viste el primer número: 45 llamadas ahorradas y 82.1 hilos-segundo no quemados sobre shipping caído, con recuperación automática cuando revive.
Antes de avanzar deberías poder: explicar la metáfora del interruptor eléctrico y por qué "dejar de llamar" protege a las dos partes; nombrar los tres estados y qué hace cada uno; y distinguir un fallo transitorio (dominio del retry) de uno persistente (dominio del breaker).
Lo que sigue es dejar de anunciar y medir. La lección 2 monta la simulación de orders→shipping sin breaker, provoca el apagón de 50 segundos, y cuantifica el desperdicio: las 50 llamadas que esperan el timeout, los hilos-segundo quemados, los hilos ocupados que no atienden a nadie más. Ese número es el "antes" contra el cual medirás cada mejora del módulo —y el que hace que el breaker deje de parecer una idea elegante y empiece a parecer una necesidad.
Recursos
- Michael T. Nygard, Release It! Design and Deploy Production-Ready Software, 2ª ed. (Pragmatic Bookshelf, 2018) — la fuente original del patrón. Nygard acuñó el nombre "Circuit Breaker" y la metáfora del interruptor eléctrico en este libro; su capítulo sobre stability patterns es la referencia canónica y la razón por la que hoy hablamos de "abrir" y "cerrar" un circuito. En inglés.
- Martin Fowler, "CircuitBreaker" — martinfowler.com/bliki/CircuitBreaker.html. La explicación de referencia de la máquina de estados con un ejemplo de código mínimo; el texto que más ha difundido el patrón fuera del libro de Nygard. Gratis y en inglés.
- Documentación de resilience4j: "CircuitBreaker" — resilience4j.readme.io/docs/circuitbreaker. La biblioteca de resiliencia estándar del ecosistema Java hoy (sucesora de Hystrix); su documentación describe los tres estados y los parámetros reales (
failureRateThreshold,waitDurationInOpenState) que usaremos en versión simplificada. En inglés. - Documentación de Polly (.NET): "Circuit Breaker resilience strategy" — learn.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/implement-circuit-breaker-pattern. La implementación de referencia en el ecosistema .NET, con la misma máquina de estados; útil para ver que el patrón es idéntico entre lenguajes. En inglés.