Módulo 5: Modos de fallo y resiliencia para IA

El circuit breaker sobre el modelo

Descripción

El fallback (lección 5) resuelve qué respondes cuando el modelo falla. Esta lección resuelve una pregunta que queda abierta: durante una caída del modelo que dura minutos, ¿tiene sentido intentar el modelo en cada request —esperar el timeout, fallar, caer al fallback— una y otra vez? No. Si el modelo lleva veinte fallos seguidos, el vigésimo primer request casi seguro también va a fallar; intentarlo solo te cuesta el timeout (latencia), el dinero de la llamada, y —esto es propio de la IA— más carga sobre un modelo que quizás está rate-limited justo por exceso de llamadas. La pieza que resuelve esto es el circuit breaker: un componente que detecta que el modelo viene fallando y deja de llamarlo por un rato, yendo directo al fallback, hasta que un intento de prueba confirme que el modelo se recuperó. Esta lección lo aplica al componente de IA y —lo propio de aquí— resalta por qué un breaker sobre un modelo tiene una motivación extra que un breaker sobre un servicio clásico no tiene: el costo por llamada y las cuotas del proveedor.

En la lección 1 viste el breaker abrirse en la traza del incidente; aquí lo desarrollas a fondo y mides su efecto. Vas a ver, ejecutado, dos estrategias frente a una caída del modelo: una que reintenta ciegamente en cada request, y otra con circuit breaker. La segunda recorta las llamadas al modelo, los timeouts pagados, y el costo, mientras ambas responden el 100% de los requests (gracias al fallback).

Conexión con el módulo. Esta lección corona la familia de disponibilidad: el timeout (L4) acota cada espera individual, el fallback (L5) da la ruta alterna, y el breaker (esta) evita el desperdicio de intentar una y otra vez una ruta que sabemos rota. Las tres trabajan juntas —lo viste en el ejemplo de la lección 1—. La frontera con la guía de resiliencia es dura y aquí es explícita: la mecánica del circuit breaker —los estados con precisión, las ventanas deslizantes de conteo, los umbrales, cómo se calibra— se enseña en resilience-and-reliability-patterns-guide M5 (circuit breakers); aquí lo aplicamos al modelo y remitimos allá para la implementación seria. Nuestro breaker es una versión mínima, suficiente para ver el patrón.

Una analogía: el interruptor que corta la luz para proteger la casa

Un circuit breaker —el nombre viene de aquí— es, literalmente, el interruptor termomagnético del tablero eléctrico de tu casa. Su trabajo: cuando detecta que por un circuito pasa demasiada corriente —un corto, un aparato descompuesto—, corta la electricidad de ese circuito. No lo hace para molestarte; lo hace para proteger: sin el breaker, esa sobrecorriente recalentaría los cables y podría iniciar un incendio. El breaker prefiere dejarte sin luz en la cocina un rato a dejar que se queme la casa. Y fíjate en el detalle inteligente: cuando arreglas el problema, no compras un breaker nuevo —lo rearmas: lo subes de nuevo y, si el circuito ya está bien, la luz vuelve; si el corto sigue, se vuelve a botar—. Ese "intento de rearme" es exactamente el estado HALF_OPEN.

Ahora traslada la imagen a tu sistema llamando a un modelo caído. Sin breaker, cada request "empuja corriente" por un circuito roto: intenta el modelo, espera el timeout, falla, cae al fallback. Con veinte requests por segundo durante una caída de cinco minutos, son miles de llamadas inútiles a un modelo que ya sabes caído —miles de timeouts pagados, miles de llamadas cobradas, y miles de golpes a un proveedor que quizás está caído precisamente porque está saturado de llamadas—. El breaker es el interruptor que dice: "detecté que este circuito viene fallando; corto las llamadas al modelo, mando todo al fallback, y cada tanto pruebo si ya se recuperó". Deja de empujar corriente por el circuito roto. Protege tu latencia, tu presupuesto, y —lo específico de la IA— la cuota del proveedor.

Aquí está el punto: el circuit breaker sobre el modelo deja de golpear una ruta que sabe rota, para no desperdiciar recursos ni empeorar el problema, y se rearma solo cuando la ruta se recupera. Es la diferencia entre un empleado terco que sigue marcando un teléfono descompuesto cada dos segundos toda la tarde, y uno sensato que, tras varios intentos fallidos, deja de marcar por diez minutos y usa otro canal, volviendo a probar de vez en cuando.

Ejemplo trabajado: reintento ciego vs circuit breaker

Vamos a medir el desperdicio que evita el breaker. Simulamos 16 requests con una caída del modelo del request 3 al 11 (nueve requests seguidos fallando —una caída, no fallos sueltos—, como una ventana de rate limit o una interrupción del proveedor). Cada llamada al modelo que falla cuesta un timeout (800 ms perdidos) y cuenta como una llamada cobrada ($0.002). Comparamos:

  • Sin breaker (reintento ciego): cada request llama al modelo, pase lo que pase. Durante la caída, paga el timeout completo cada vez.
  • Con breaker: tras 3 fallos seguidos, el breaker se abre y deja de llamar al modelo; los requests van directo al fallback. Cada 4 requests, deja pasar un intento de prueba (HALF_OPEN) para ver si el modelo se recuperó.

Ambas estrategias responden el 100% gracias al fallback; lo que cambia es el desperdicio —llamadas, timeouts, costo—.

# Leccion 6: CIRCUIT BREAKER sobre el modelo. Si el modelo viene fallando,
# deja de llamarlo y usa el fallback. Especifico de IA: cada llamada fallida
# cuesta latencia Y dinero, y reintentar un modelo rate-limited lo EMPEORA
# (mas 429, mas cuota quemada). Mecanica del breaker a fondo:
# resilience-and-reliability-patterns-guide M5.

MODEL_COST = 0.002    # $ por intento de llamada al modelo
TIMEOUT_MS = 800      # ms perdidos cada vez que un fallo agota el timeout


class ModelError(Exception):
    pass


OUTAGE = set(range(3, 12))   # el modelo cae del request 3 al 11 (rate-limited)
N = 16


def call_model(i):
    if i in OUTAGE:
        raise ModelError("caido / rate-limited")
    return "ok"


def fallback(i):
    return "fallback"


class CircuitBreaker:
    def __init__(self, fail_threshold=3, cooldown=4):
        self.fail_threshold = fail_threshold
        self.cooldown = cooldown
        self.fails = 0
        self.state = "CLOSED"
        self.opened_at = None

    def allow(self, now):
        if self.state == "OPEN":
            if now - self.opened_at >= self.cooldown:
                self.state = "HALF_OPEN"
                return True
            return False
        return True

    def on_success(self):
        self.fails = 0
        self.state = "CLOSED"

    def on_failure(self, now):
        self.fails += 1
        if self.fails >= self.fail_threshold:
            self.state = "OPEN"
            self.opened_at = now


# --- Estrategia 1: SIN breaker (reintento ciego, golpea al modelo siempre) ---
calls_naive = timeouts_naive = 0
for i in range(N):
    try:
        call_model(i)
    except ModelError:
        timeouts_naive += 1   # pago el timeout completo
        fallback(i)
    calls_naive += 1          # llame al modelo en TODOS los requests

# --- Estrategia 2: CON breaker ---
cb = CircuitBreaker(fail_threshold=3, cooldown=4)
calls_cb = timeouts_cb = skipped_cb = 0
print(f"{'req':<5}{'breaker':<11}{'llamo al modelo?':<18}resultado")
print("-" * 52)
for i in range(N):
    if cb.allow(i):
        state = cb.state
        try:
            call_model(i)
            cb.on_success()
            calls_cb += 1
            print(f"{i:<5}{state:<11}{'si':<18}modelo ok")
        except ModelError:
            calls_cb += 1
            timeouts_cb += 1
            cb.on_failure(i)
            fallback(i)
            print(f"{i:<5}{state:<11}{'si':<18}fallo -> fallback")
    else:
        skipped_cb += 1
        fallback(i)
        print(f"{i:<5}{'OPEN':<11}{'no (evitado)':<18}fallback directo")

print("-" * 52)
print("Comparacion (los 16 requests respondieron en ambas estrategias):")
print(f"  {'':<26}{'sin breaker':>14}{'con breaker':>14}")
print(f"  {'llamadas al modelo':<26}{calls_naive:>14}{calls_cb:>14}")
print(f"  {'timeouts pagados':<26}{timeouts_naive:>14}{timeouts_cb:>14}")
print(f"  {'ms perdidos en timeouts':<26}{timeouts_naive * TIMEOUT_MS:>14}"
      f"{timeouts_cb * TIMEOUT_MS:>14}")
print(f"  {'costo de llamadas ($)':<26}{calls_naive * MODEL_COST:>14.3f}"
      f"{calls_cb * MODEL_COST:>14.3f}")
print(f"  llamadas evitadas por el breaker: {skipped_cb}  "
      f"(no golpearon un modelo ya rate-limited)")

Qué esperar. Al correr el archivo, la salida es exactamente esta:

req  breaker    llamo al modelo?  resultado
----------------------------------------------------
0    CLOSED     si                modelo ok
1    CLOSED     si                modelo ok
2    CLOSED     si                modelo ok
3    CLOSED     si                fallo -> fallback
4    CLOSED     si                fallo -> fallback
5    CLOSED     si                fallo -> fallback
6    OPEN       no (evitado)      fallback directo
7    OPEN       no (evitado)      fallback directo
8    OPEN       no (evitado)      fallback directo
9    HALF_OPEN  si                fallo -> fallback
10   OPEN       no (evitado)      fallback directo
11   OPEN       no (evitado)      fallback directo
12   OPEN       no (evitado)      fallback directo
13   HALF_OPEN  si                modelo ok
14   CLOSED     si                modelo ok
15   CLOSED     si                modelo ok
----------------------------------------------------
Comparacion (los 16 requests respondieron en ambas estrategias):
                               sin breaker   con breaker
  llamadas al modelo                    16            10
  timeouts pagados                       9             4
  ms perdidos en timeouts             7200          3200
  costo de llamadas ($)              0.032         0.020
  llamadas evitadas por el breaker: 6  (no golpearon un modelo ya rate-limited)

Lee la traza y luego la comparación, porque ahí está el breaker completo.

La traza muestra el ciclo de vida del breaker. Sigue los estados:

  • Requests 0-2 (CLOSED): el modelo está sano, el breaker deja pasar todo, las llamadas salen ok.
  • Requests 3-5 (CLOSED, fallando): empieza la caída. El breaker aún está cerrado —no sabía—, así que llama al modelo, falla, y cae al fallback cada vez, contando fallos: 1, 2, 3. Al tercer fallo, se abre.
  • Requests 6-8 (OPEN): el breaker está abierto. No llama al modelo —"no (evitado)"—, va directo al fallback. Aquí está el ahorro: tres requests que no pagaron timeout ni costo porque el breaker ya sabía que el modelo estaba caído.
  • Request 9 (HALF_OPEN): pasó el enfriamiento (4 requests), el breaker deja pasar un intento de prueba. El modelo todavía está caído (la caída va hasta el 11), así que el intento falla y el breaker se vuelve a abrir. Es el "rearme" que se bota de nuevo porque el corto sigue.
  • Requests 10-12 (OPEN): de vuelta abierto, evitando llamadas.
  • Request 13 (HALF_OPEN): otro intento de prueba. Ahora el modelo ya se recuperó (la caída terminó en el 11), así que el intento funciona, y el breaker se cierra (CLOSED).
  • Requests 14-15 (CLOSED): el sistema recuperado, funcionando normal. Se rearmó solo.

La comparación mide el desperdicio evitado. Ambas estrategias respondieron los 16 requests (el fallback siempre está). Pero mira el costo del reintento ciego frente al breaker:

  • Llamadas al modelo: 16 vs 10. El breaker evitó 6 llamadas a un modelo caído.
  • Timeouts pagados: 9 vs 4. Sin breaker, pagaste el timeout en los 9 requests de la caída; con breaker, solo en los 4 intentos reales (3 antes de abrir + 1 de prueba fallido). 5 timeouts menos.
  • Milisegundos perdidos: 7200 vs 3200. El breaker ahorró 4 segundos de espera acumulada.
  • Costo: $0.032 vs $0.020. El breaker ahorró un 37% del costo durante la caída, al no cobrar las 6 llamadas evitadas.

La implicación arquitectónica, y aquí está lo específico de la IA: cada llamada evitada por el breaker no es solo latencia ahorrada; es dinero ahorrado y carga que no le pusiste a un modelo que ya estaba mal. En un servicio clásico, el breaker "solo" te ahorra latencia y protege tus hilos. En un modelo, te ahorra dinero (cada llamada cuesta tokens) y —lo más sutil— evita que empeores un rate limit: si el modelo está caído porque está saturado de llamadas, seguir llamándolo prolonga su saturación. El breaker corta ese círculo vicioso. Por eso un breaker sobre un modelo tiene una motivación que un breaker clásico no tiene.

Profundización: el breaker aplicado a un componente de IA

Los tres estados, en breve (la mecánica a fondo está en la guía de resiliencia). El breaker es una pequeña máquina de estados:

  • CLOSED (cerrado, funcionando): las llamadas pasan al modelo normalmente. Se cuentan los fallos.
  • OPEN (abierto, protegiendo): tras cruzar el umbral de fallos, el breaker se abre y corta las llamadas al modelo; todo va directo al fallback. Se mantiene así durante un enfriamiento.
  • HALF_OPEN (medio abierto, probando): pasado el enfriamiento, deja pasar un intento de prueba. Si funciona, se cierra (recuperado); si falla, se vuelve a abrir (sigue caído).

Nuestra implementación es mínima: cuenta fallos consecutivos y usa un enfriamiento fijo en número de requests. Una implementación de producción usa ventanas de tiempo, umbrales por proporción de fallos (no solo consecutivos), y consideraciones de concurrencia. Todo eso es la guía de resiliencia M5. Aquí basta la versión mínima para ver el patrón y su efecto.

El breaker necesita al timeout y al fallback; no funciona solo. Retén la interdependencia de las tres piezas de disponibilidad:

  • Sin timeout (L4), un cuelgue no lanza excepción, así que el breaker no cuenta el fallo —se queda esperando, igual que sin breaker—. El timeout convierte el cuelgue en el fallo que el breaker cuenta.
  • Sin fallback (L5), cuando el breaker está abierto y no llama al modelo, no tiene qué responder —el breaker solo te da un error más rápido—. El fallback es lo que el breaker sirve cuando corta.
  • El breaker es lo que evita pagar el timeout y el costo del fallback una y otra vez durante una caída larga.

Las tres juntas: el timeout acota cada intento, el breaker deja de intentar cuando es inútil, el fallback responde. Quita una y las otras cojean.

El ángulo de IA que no está en un breaker clásico: el rate limit. Vale insistir porque es lo propio de esta lección. Un servicio clásico caído normalmente no empeora porque lo sigas llamando (ya está caído, tus llamadas rebotan). Un modelo rate-limited sí empeora: cada llamada durante la ventana de rate limit cuenta contra tu cuota y puede extender el bloqueo, y si compartes cuota entre features, una feature que reintenta ciegamente puede rate-limitar a las demás. El breaker es la defensa correcta contra un 429 justamente porque deja de consumir cuota mientras espera. Reintentar un 429 (aun con backoff) sigue consumiendo; abrir el breaker no. Por eso, para el rate limit, el breaker no es solo una optimización de latencia: es la diferencia entre recuperarte en un minuto o quedarte bloqueado diez.

El tradeoff honesto: el breaker retrasa la detección de la recuperación. Nada es gratis. Mira el request 12 de la traza: el modelo ya se había recuperado en el 11 (fin de la caída), pero el breaker estaba OPEN en el enfriamiento, así que sirvió fallback en el 12 aunque el modelo ya funcionaba —una respuesta degradada innecesaria—. El breaker no probó hasta el 13. Ese es el costo: durante el enfriamiento, sirves algún fallback de más porque no sabes que el modelo volvió. Es un tradeoff deliberado: un enfriamiento corto detecta la recuperación rápido pero prueba más seguido (más llamadas de prueba, más riesgo de reabrir en falso); un enfriamiento largo protege más pero tarda en notar la recuperación. La guía de resiliencia trata cómo calibrarlo. La lección: el breaker cambia "muchas llamadas inútiles" por "alguna respuesta degradada de más", un cambio casi siempre favorable.

Errores comunes

Reintentar ciegamente durante una caída (sin breaker). Qué pasa: el modelo tiene una caída de cinco minutos, y el sistema intenta el modelo en cada uno de los miles de requests de esos cinco minutos —cada uno paga el timeout, cada uno cuesta, y todos juntos martillan a un proveedor que quizás está caído por sobrecarga—. Por qué pasa: se tiene fallback pero no breaker; cada request "descubre" la caída por su cuenta, pagando el timeout. Cómo detectarlo: durante una caída, tu tasa de llamadas al modelo no baja —sigues llamando igual que cuando estaba sano—. Cómo corregirlo: pon un circuit breaker que, tras N fallos, deje de llamar y vaya directo al fallback. El ejemplo lo mide: el breaker recortó las llamadas de 16 a 10 y el costo un 37%.

Reintentar un rate limit y empeorarlo. Qué pasa: la API devuelve 429, el sistema reintenta —quizás con backoff, quizás no—, y cada reintento consume más cuota, extendiendo el bloqueo; un rate limit que habría durado un minuto dura diez porque no dejaste de llamar. Por qué pasa: se trata el 429 como un error transitorio que "un reintento arregla", sin ver que este error se alimenta de los reintentos. Cómo detectarlo: tu manejo del 429 sigue llamando al modelo (reintento o no) en vez de cortar. Cómo corregirlo: ante un 429 persistente, abre el breaker —deja de llamar por completo mientras esperas que la ventana de cuota se libere— y sirve el fallback. El breaker es la única defensa que deja de consumir cuota.

Un breaker sin enfriamiento razonable (que nunca prueba o prueba demasiado). Qué pasa: dos variantes. Un enfriamiento eterno deja el breaker abierto para siempre —el modelo se recuperó hace horas y sigues sirviendo fallback porque nunca probaste—. Un enfriamiento nulo prueba en cada request —básicamente no tienes breaker, sigues martillando—. Por qué pasa: el enfriamiento se puso al azar sin pensar el tradeoff detección-vs-protección. Cómo detectarlo: o tu breaker se queda abierto mucho después de la recuperación, o sigues llamando al modelo caído casi como si no hubiera breaker. Cómo corregirlo: calibra el enfriamiento según cuánto dura típicamente una caída y qué tan caro es un intento —la guía de resiliencia M5 lo formaliza—; el HALF_OPEN debe probar lo bastante seguido para notar la recuperación, pero no tanto que martillee.

Ejercicios

Ejercicio 1 — Sigue el estado del breaker. Usando la máquina de estados del ejemplo (umbral 3 fallos, enfriamiento 4), traza qué estado tendría el breaker y si llamaría al modelo en cada uno de estos requests, dado que el modelo está caído del request 2 al 6 (y sano el resto): requests 0, 1, 2, 3, 4, 5, 6, 7.

Ver solución

Modelo caído en 2, 3, 4, 5, 6. Umbral 3, enfriamiento 4.

  • req 0 (CLOSED): llama, modelo sano, ok. fails=0.
  • req 1 (CLOSED): llama, sano, ok. fails=0.
  • req 2 (CLOSED): llama, falla (caída). fails=1. Sigue CLOSED.
  • req 3 (CLOSED): llama, falla. fails=2. Sigue CLOSED.
  • req 4 (CLOSED): llama, falla. fails=3 → se abre (opened_at=4). Sirve fallback.
  • req 5 (OPEN): 5-4=1 < 4 → no llama, fallback directo.
  • req 6 (OPEN): 6-4=2 < 4 → no llama, fallback directo. (El modelo aún está caído, así que evitar la llamada fue correcto.)
  • req 7 (OPEN): 7-4=3 < 4 → no llama, fallback directo. (El modelo ya se recuperó en el 7, pero el breaker no lo sabe porque aún no toca probar; sirve fallback de más —el tradeoff del enfriamiento—.)

Resumen: llamó al modelo en 0,1,2,3,4 (5 veces), evitó llamarlo en 5,6,7 (3 veces). Pagó timeout en 2,3,4 (3 veces). El breaker probaría de nuevo en el request 8 (8-4=4 ≥ 4 → HALF_OPEN), donde el modelo ya sano cerraría el breaker. Nota cómo en el req 7 el sistema sirvió fallback aunque el modelo ya funcionaba: ese es el costo del enfriamiento, el tradeoff honesto de la profundización.

Ejercicio 2 — Por qué el breaker sobre un modelo es distinto. Un compañero dice: "un circuit breaker es un circuit breaker; da igual que esté sobre un servicio de pagos o sobre un modelo". Da dos razones, específicas de un componente de IA, por las que la motivación de poner un breaker sobre un modelo es más fuerte que sobre un servicio clásico.

Ver solución

Dos razones específicas de la IA:

  1. Cada llamada cuesta dinero. Un modelo se cobra por token/por llamada. Cuando el reintento ciego martillea a un modelo caído con miles de llamadas fallidas, muchas de esas llamadas igual se cobran (si el proveedor procesó tokens antes de fallar, o simplemente por el intento). Un servicio de pagos clásico normalmente no te cobra por una llamada que rebotó. Así que el breaker sobre un modelo ahorra un recurso —dinero— que el breaker clásico no ahorra. El ejemplo lo midió: 37% de costo ahorrado durante la caída.

  2. El rate limit empeora al llamar. Un modelo suele estar "caído" por estar rate-limited (429) —una cuota, no una avería—. Cada llamada durante la ventana de rate limit consume cuota y puede extender el bloqueo, y si varias features comparten cuota, la que reintenta puede rate-limitar a las demás. Un servicio clásico caído normalmente no empeora porque lo sigas llamando. Así que sobre un modelo, seguir llamando no es solo desperdicio: es contraproducente, alimenta el problema. El breaker, al dejar de consumir cuota, es la defensa que permite que la ventana de rate limit se libere.

En ambos casos, el breaker clásico ahorra latencia y protege hilos; el breaker sobre un modelo, además, ahorra dinero y evita empeorar el propio fallo. La motivación es estrictamente mayor.

Ejercicio 3 — Las tres piezas juntas. Explica, para un sistema con timeout + fallback + circuit breaker, qué pieza falla (y qué consecuencia tiene) si quitas cada una, dejando las otras dos. Completa: (a) timeout + fallback, sin breaker; (b) fallback + breaker, sin timeout; (c) timeout + breaker, sin fallback.

Ver solución
  • (a) timeout + fallback, sin breaker: el sistema funciona pero desperdicia. Cada request durante una caída larga descubre el fallo por su cuenta: intenta el modelo, paga el timeout (acotado por el timeout, eso sí), falla, cae al fallback. Responde el 100%, pero paga miles de timeouts y llamadas cobradas innecesarias, y martillea al proveedor. Consecuencia: latencia y costo desperdiciados durante toda la caída, y posible empeoramiento de un rate limit. Es la "estrategia sin breaker" del ejemplo.
  • (b) fallback + breaker, sin timeout: se rompe ante un cuelgue. Un modelo que se cuelga (no lanza excepción, solo espera) hace que el request se quede bloqueado indefinidamente. El breaker no cuenta el fallo porque no hubo excepción —se queda esperando igual—, así que nunca se abre; el fallback no se dispara porque no hubo excepción que atrapar. Consecuencia: el sistema se congela ante un cuelgue, aunque tengas breaker y fallback. El timeout es lo que convierte el cuelgue en el fallo que los otros dos necesitan.
  • (c) timeout + breaker, sin fallback: el sistema falla más rápido, pero falla. El timeout acota la espera, el breaker deja de intentar el modelo caído... pero cuando el breaker corta o el timeout vence, no hay a qué degradar: el request se cae con un error (más rápido y más limpio que sin timeout, pero se cae). Consecuencia: alta disponibilidad de fallar rápido, no de responder. Sin fallback, el breaker solo te da un error veloz. El fallback es lo que convierte "falla rápido" en "responde peor".

La lección: las tres son interdependientes. El timeout crea el fallo, el fallback lo responde, el breaker evita repetir el intento inútil. Ninguna sola basta.

Resumen y siguiente paso

En esta lección aplicaste el circuit breaker al componente de IA: la pieza que detecta que el modelo viene fallando y deja de llamarlo por un rato, yendo directo al fallback, hasta que un intento de prueba confirme que se recuperó. Lo viste con el interruptor eléctrico que corta la luz para proteger la casa y se rearma solo, y lo mediste: frente a una caída del modelo, el breaker recortó las llamadas de 16 a 10, los timeouts de 9 a 4, la espera de 7200 a 3200 ms, y el costo de $0.032 a $0.020 —un 37%—, mientras ambas estrategias respondían el 100% gracias al fallback. Seguiste el ciclo CLOSED → OPEN → HALF_OPEN → CLOSED en la traza, con su intento de prueba que falla mientras el modelo sigue caído y funciona cuando se recupera. Y retuviste lo propio de la IA: un breaker sobre un modelo ahorra dinero y evita empeorar un rate limit, motivaciones que un breaker clásico no tiene.

Antes de avanzar deberías poder: explicar los tres estados del breaker y qué hace en cada uno; argumentar por qué las tres piezas de disponibilidad (timeout, fallback, breaker) se necesitan mutuamente; dar dos razones específicas de IA por las que un breaker sobre un modelo tiene una motivación extra; y reconocer el tradeoff del enfriamiento. La mecánica del breaker a fondo vive en resilience-and-reliability-patterns-guide M5.

Con la lección 6 cierras la familia de fallos de disponibilidad (ruidosos) y sus tres defensas. La lección 7 vuelve a los fallos silenciosos, pero al que no habías visto: el drift —la degradación silenciosa en el tiempo—. No es una caída ni una excepción: el modelo o los datos cambian poco a poco y la calidad baja sin que nada se rompa. Vas a ver, ejecutado, el eval de la búsqueda bajar de 0.94 a 0.75 en siete semanas a medida que llegan queries de categorías nuevas, y un monitor que dispara la alerta en la semana 4 —dos semanas antes de que los usuarios se quejen—. La forma de que la degradación lenta no te tome por sorpresa.

Recursos

  • resilience-and-reliability-patterns-guide (este ecosistema), M5 "Circuit breakers" — la referencia central para la mecánica del breaker que aquí solo aplicamos: los estados con precisión, las ventanas deslizantes de conteo, los umbrales por proporción, la calibración del enfriamiento, la concurrencia. Cuando implementes un breaker de verdad, ese es el destino. En español.
  • Anthropic, documentación de Claude — docs.anthropic.com. Las páginas de rate limits describen las cuotas por tokens y requests y el comportamiento del 429 —el modo de fallo que hace del breaker sobre un modelo algo más que una optimización de latencia—. Conceptual, sin fijar versión. En inglés.
  • Martin Fowler, "CircuitBreaker" — martinfowler.com/bliki/CircuitBreaker.html. El artículo clásico que da nombre y forma al patrón; útil para el modelo mental de los estados. En inglés.
  • Chip Huyen, AI Engineering (O'Reilly, 2024). Los capítulos sobre confiabilidad y costo de inferencia tratan las cuotas del proveedor y el manejo de fallos de disponibilidad de un modelo como propiedades de diseño. En inglés.