Explica el patrón circuit breaker — ¿cuáles son sus estados y cuándo ocurre cada transición?
Lleva el nombre del fusible de tu casa por una razón: un circuit breaker impide que un llamador siga golpeando una dependencia que falla, fallando rápido en lugar de acumular hilos que nunca regresan.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Explica el patrón circuit breaker — ¿cuáles son sus estados y cuándo ocurre cada transición?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#La caja de fusibles para las llamadas entre servicios
Su nombre viene directamente del fusible eléctrico doméstico, y la analogía es casi exacta: un circuit breaker se sitúa entre quien llama y una dependencia, y cuando esa dependencia empieza a fallar, el breaker "salta" y deja de enviarle corriente —peticiones— en lugar de permitir que cada llamador siga golpeando un servicio que ya está en problemas. Sin uno, una dependencia lenta o que está fallando hace que los llamadores acumulen hilos, conexiones o memoria esperándola; en cuanto el llamador agota su propio pool de recursos, él mismo se vuelve lento o no disponible, y el fallo se propaga en cascada por todo el grafo de llamadas. El trabajo del circuit breaker no es arreglar la dependencia — es fallar rápido, proteger los propios recursos del llamador, y darle a la dependencia espacio para recuperarse.
#Los tres estados
Closed (cerrado) es la operación normal: cada petición se deja pasar hacia la dependencia real. Aun así, el breaker no está inactivo — cuenta resultados sobre una ventana móvil (ya sean las últimas N llamadas o los últimos T segundos) y calcula una tasa de fallo en vivo. En el momento en que esa tasa supera un umbral configurado (digamos, el 50% de las últimas 20 llamadas falló, o 10 fallos en 10 segundos), el breaker salta a Open.
Open (abierto) deja de enviar tráfico real por completo: cada llamada falla de inmediato —típicamente lanzando un error de "circuito abierto" o invocando un fallback— sin llegar siquiera a tocar la red. Este es el comportamiento de "fallar rápido" que protege al llamador: en lugar de que un hilo se bloquee durante un timeout de 30 segundos contra una dependencia que ya está caída, falla en microsegundos. El breaker permanece Open durante un tiempo de espera de reinicio configurado (a veces llamado sleep window), y luego transiciona por sí solo a Half-Open — nadie tiene que volverlo a activar manualmente.
Half-Open (semiabierto) es una sonda: el breaker deja pasar un número pequeño de peticiones de prueba hacia la dependencia real (a menudo solo una) mientras sigue bloqueando todo lo demás. Si la(s) prueba(s) tienen éxito, el breaker asume que la dependencia se recuperó, reinicia sus contadores y vuelve a Closed. Si una prueba falla, asume que la dependencia sigue rota, vuelve directamente a Open y reinicia el tiempo de espera — no pasa por una rampa lenta, ni deja pasar más tráfico solo porque haya transcurrido algo de tiempo.
#De qué no es un sustituto
Un circuit breaker sin un timeout por llamada sobre la petición subyacente es prácticamente inútil: si una llamada a una dependencia colgada nunca retorna, el breaker no puede contarla como fallo hasta que el timeout se dispare, así que no puede saltar a tiempo para ayudar — el timeout es lo que convierte "quedarse colgado" en "fallar", que es lo único a lo que un breaker puede reaccionar. Los circuit breakers también se combinan naturalmente con el bulkheading (darle a cada dependencia su propio pool de hilos o conexiones, para que el agotamiento de recursos de una dependencia no pueda dejar sin hilos las llamadas a una dependencia sana) — un breaker te impide seguir llamando a una dependencia que está fallando, pero solo un bulkhead impide que esa dependencia ya se haya comido todos tus hilos antes de que el breaker tuviera suficientes datos para saltar.
Un matiz de nivel senior que vale la pena nombrar explícitamente: no todo error debería contar en contra del breaker. Un 500 de la dependencia, una conexión rechazada, o un timeout son señales de que la dependencia misma no está sana y deben contar. Un 400 causado por una petición mal formada de nuestro propio lado es un bug de nuestro lado — reintentarlo no ayudará y no se arregla dándole un descanso a la dependencia, así que contarlo hacia el umbral de disparo abriría el circuito por la razón equivocada y negaría servicio innecesariamente a peticiones que sí habrían tenido éxito.
#Una implementación real
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';interface CircuitBreakerOptions {failureThreshold: number; // fallos necesarios dentro de la ventana para saltarwindowSizeMs: number; // ventana móvil para contar fallosresetTimeoutMs: number; // cuánto tiempo permanecer Open antes de sondearisFailure?: (err: unknown) => boolean; // clasifica qué errores cuentan}export class CircuitBreaker {private state: CircuitState = 'CLOSED';private failureTimestamps: number[] = [];private openedAt = 0;constructor(private options: CircuitBreakerOptions) {}async execute<T>(fn: () => Promise<T>, fallback?: () => T): Promise<T> {if (this.state === 'OPEN') {if (Date.now() - this.openedAt >= this.options.resetTimeoutMs) {this.state = 'HALF_OPEN';} else {return this.handleRejection(fallback);}}try {const result = await fn();this.onSuccess();return result;} catch (err) {const counts = this.options.isFailure ? this.options.isFailure(err) : true;if (counts) {this.onFailure();}if (fallback) {return fallback();}throw err;}}private onSuccess(): void {if (this.state === 'HALF_OPEN') {this.state = 'CLOSED';this.failureTimestamps = [];}// En estado CLOSED un éxito no necesita manejo especial --// la ventana móvil hace que los fallos viejos se descarten solos, abajo.}private onFailure(): void {const now = Date.now();if (this.state === 'HALF_OPEN') {// La petición de prueba falló -- la dependencia sigue rota.this.tripOpen(now);return;}this.failureTimestamps.push(now);this.failureTimestamps = this.failureTimestamps.filter((t) => now - t <= this.options.windowSizeMs);if (this.failureTimestamps.length >= this.options.failureThreshold) {this.tripOpen(now);}}private tripOpen(now: number): void {this.state = 'OPEN';this.openedAt = now;this.failureTimestamps = [];}private handleRejection<T>(fallback?: () => T): T {if (fallback) {return fallback();}throw new Error('El circuito está OPEN -- llamada rechazada sin contactar la dependencia');}getState(): CircuitState {return this.state;}}// Uso:const inventoryBreaker = new CircuitBreaker({failureThreshold: 5,windowSizeMs: 10_000,resetTimeoutMs: 30_000,isFailure: (err) => !(err instanceof ValidationError), // nuestros propios 400 no cuentan});async function checkStock(sku: string) {return inventoryBreaker.execute(() => inventoryClient.getStock(sku),() => ({ sku, quantity: 0, stale: true }) // degradar con elegancia en vez de lanzar);}
La forma de arriba refleja lo que librerías de producción como resilience4j (Java/Kotlin) o Polly (.NET) ofrecen de fábrica, además de lo que popularizó Hystrix antes de que Netflix lo diera de baja en favor de resilience4j: una ventana móvil de fallos, un enfriamiento, un estado de sondeo, y un fallback conectable. Lo que más suelen hacer mal las versiones artesanales es olvidar el paso de clasificación isFailure y disparar el breaker con errores en los que la dependencia no tuvo ninguna responsabilidad.
| Estado | ¿Envía tráfico real? | Condición de salida |
|---|---|---|
| Closed | Sí | La tasa de fallo supera el umbral dentro de la ventana -> Open |
| Open | No — falla rápido / usa fallback | Se cumple el tiempo de espera de reinicio -> Half-Open |
| Half-Open | Solo una prueba limitada | La prueba tiene éxito -> Closed; la prueba falla -> Open (el tiempo de espera se reinicia) |
:::analogy Se llama así por lo que tienes en la pared de tu casa, y no es casualidad Un breaker doméstico no arregla un cortocircuito — desconecta el circuito para que el cortocircuito no queme la casa, y alguien puede volver a activarlo una vez resuelta la falla. Un circuit breaker de software desconecta una dependencia que está fallando para que no queme al llamador, y se vuelve a activar solo para probar si la falla ya se resolvió. :::
:::warning Un circuit breaker sin timeout en la llamada no puede saltar a tiempo para servir de algo Si la llamada envuelta puede quedarse colgada indefinidamente, el breaker nunca acumula suficientes fallos con la rapidez necesaria para abrirse — los hilos del llamador ya están agotados para cuando el conteo de fallos por fin se pone al día. Siempre configura un timeout agresivo en la llamada subyacente misma; el breaker reacciona a los fallos, no los crea. :::
:::key Tres estados, tres trabajos Closed observa y deja pasar el tráfico. Open protege al llamador fallando rápido sin tocar la dependencia. Half-Open sondea, con el radio de impacto más pequeño posible, si es seguro volver a Closed. :::
🧠 Check your understanding
0/3 · 0/3 answered1. Un circuit breaker ha estado Open durante exactamente su tiempo de espera de reinicio configurado. ¿Qué ocurre con la siguiente petición?
2. ¿Por qué se considera que un timeout por llamada es un prerrequisito para que un circuit breaker funcione correctamente, y no solo un extra deseable?
3. ¿Por qué un 400 Bad Request causado por una entrada mal formada del llamador típicamente NO debería contar para disparar el breaker?