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

El modelo caído, lento o rate-limited

Descripción

Cuando pusiste el LLM tras una frontera (módulo 1), esa frontera escondía algo que ahora hay que mirar de frente: al otro lado vive una API externa que no controlas, servida por un tercero, con su propia disponibilidad, su propia latencia variable, y sus propias cuotas. Un componente que corre dentro de tu proceso falla poco y falla rápido; un componente que vive detrás de una llamada de red a un proveedor de modelos falla de tres formas nuevas que hay que diseñar explícitamente: caído (la API no responde, devuelve un 5xx), lento (tarda más que tu presupuesto, y si no haces nada, tu sistema se queda esperando), y rate-limited (agotaste tu cuota de tokens o de requests, y la API te devuelve un 429). Esta lección instala la primera defensa de esta familia, la más básica y la más olvidada: el timeout —no esperar para siempre—, y explica por qué depender de una API de modelo lenta y con cuotas cambia el diseño.

En la lección 2 clasificaste estos tres como fallos ruidosos —lanzan una excepción o un timeout los convierte en una—. En la lección 3 viste el fallo silencioso (la alucinación). Aquí volvemos al frente ruidoso con el más elemental de los problemas: la espera. Vas a ver, ejecutado, un flujo de requests donde algunas llamadas al modelo se cuelgan (30 segundos, 15 segundos) o fallan de inmediato (caído, rate-limited), y vas a medir cómo un timeout de 800 ms transforma un sistema que se bloquea 48 segundos en uno que espera 3.5 segundos y siempre responde vía fallback.

Conexión con el módulo. La lección 1 mostró la cáscara completa; esta aísla su pieza más básica —el timeout— y el problema que resuelve —la dependencia de una API lenta con cuotas—. Es el cimiento de las lecciones 5 y 6: el timeout convierte un cuelgue (que no lanza nada) en un fallo ruidoso atrapable, y solo entonces el fallback (L5) puede reaccionar y el circuit breaker (L6) puede contar fallos. Sin timeout, las otras defensas no tienen de qué agarrarse. La frontera con la guía de resiliencia es dura: la mecánica del timeout —cómo se implementa de verdad con hilos, con async, con señales, cómo se elige el valor, cómo interactúa con los reintentos— se enseña en resilience-and-reliability-patterns-guide M2 (timeouts); aquí lo aplicamos al componente de IA y remitimos allá para el detalle.

Una analogía: el teléfono que suena y suena

Imagina que llamas a un proveedor para confirmar un pedido urgente. Marcas y el teléfono suena. Suena una vez, dos, cinco, diez veces. Nadie contesta. ¿Cuánto tiempo te quedas escuchando el tono antes de colgar y buscar otra forma de resolver? Una persona sensata cuelga después de un rato razonable —treinta segundos, un minuto— y prueba otra cosa: llama a otro proveedor, manda un correo, resuelve por su cuenta. Lo que nadie hace es quedarse con el teléfono pegado a la oreja para siempre, escuchando el tono, bloqueado, sin hacer nada más, esperando a que quizás alguien conteste algún día. Eso sería absurdo: mientras esperas colgado, no atiendes a nadie más, no avanzas en nada, tu día entero se detiene por una llamada que no entró.

Un sistema sin timeout hace exactamente eso: se queda con el teléfono pegado a la oreja. Cuando llama al modelo y el modelo no responde —se colgó, la red se perdió, el proveedor está saturado—, el hilo que hizo la llamada se queda esperando indefinidamente. Y mientras espera, ese hilo no atiende otros requests; si tienes muchos requests esperando a un modelo colgado, se te agotan los hilos, se llena la cola, y un servicio que funcionaba se cae en cascada —no porque estuviera roto, sino porque todos sus recursos están esperando colgados a una llamada que nunca va a entrar—.

El timeout es la regla de "cuelgo después de X". Le dices al sistema: "espera al modelo como máximo 800 milisegundos; si no contestó, cuelga y resuelve de otra forma". Convierte una espera infinita en una espera acotada, y —esto es clave— convierte un cuelgue silencioso (el teléfono sonando, que no es ni éxito ni error, solo espera) en un fallo ruidoso que puedes atrapar y manejar (un TimeoutError que dispara el fallback). El timeout no hace que el modelo conteste más rápido; hace que tú dejes de esperarlo cuando ya no tiene sentido, para poder atender al cliente con la ruta alterna. En Mercado, es la diferencia entre una búsqueda que se congela veinte segundos porque el modelo se colgó, y una que espera 800 ms, cuelga, y te da resultados por keywords.

Ejemplo trabajado: el timeout que recorta la espera

Vamos a medir el efecto del timeout. Tenemos un flujo de 10 requests al modelo. El stub reporta, para cada uno, cuánto habría tardado (su duration_ms) y si falla de inmediato (down, rate_limited). Dos de las llamadas son cuelgues: una de 30 segundos (una API que no responde) y otra de 15 segundos. Otra es simplemente lenta (2.5 s, por encima del presupuesto). El resto responden rápido.

Comparamos dos diseños:

  • Sin timeout: esperamos la duración completa de cada llamada. Un cuelgue de 30 s nos bloquea 30 s.
  • Con timeout de 800 ms: si la llamada iba a tardar más que el presupuesto, la cortamos a los 800 ms, lanzamos ModelTimeout, y caemos al fallback. Nunca esperamos más que el presupuesto.

Importante para la reproducibilidad: el stub no duerme de verdad —no hay sleep, no hay red—; reporta una duración simulada, y el timeout se modela como "si la duración simulada excede el presupuesto, se corta a budget". Así el experimento es determinista y mide exactamente lo que queremos.

# Leccion 4: el modelo CAIDO / LENTO / RATE-LIMITED. Depender de una API
# externa lenta y con cuotas. El TIMEOUT evita esperar para siempre.
# El LLM se SIMULA: cada request trae una duracion simulada y/o un fallo;
# NO hay red ni sleeps reales. La mecanica del timeout a fondo:
# resilience-and-reliability-patterns-guide M2.

LATENCY_BUDGET_MS = 800   # presupuesto: no esperamos mas que esto


class ModelDown(Exception): pass
class RateLimited(Exception): pass
class ModelTimeout(Exception): pass


# Cada request: (kind, simulated_duration_ms). El stub NO duerme; reporta
# cuanto "habria tardado". Asi medimos el timeout de forma determinista.
REQUESTS = [
    ("ok",             120),
    ("ok",             340),
    ("slow",         30000),   # un cuelgue: 30 s (una API que no responde)
    ("rate_limited",     0),
    ("ok",             210),
    ("slow",          2500),   # lento: excede el presupuesto
    ("down",             0),
    ("ok",             180),
    ("ok",             260),
    ("slow",         15000),   # otro cuelgue
]


def call_model(kind, duration_ms):
    if kind == "down":         raise ModelDown()
    if kind == "rate_limited": raise RateLimited()
    return f"respuesta ({duration_ms} ms)"


def fallback():
    return "respuesta determinista (fallback)"


# --- Modo A: SIN timeout. Esperamos la duracion completa de cada llamada. ---
wait_A = 0
worst_A = 0
for kind, dur in REQUESTS:
    try:
        # Sin timeout, un cuelgue nos hace esperar TODA su duracion.
        if kind in ("down", "rate_limited"):
            call_model(kind, dur)      # falla de inmediato (0 ms)
        else:
            wait_A += dur
            worst_A = max(worst_A, dur)
    except (ModelDown, RateLimited):
        fallback()

# --- Modo B: CON timeout = LATENCY_BUDGET_MS. Cortamos lo que exceda. ---
wait_B = 0
worst_B = 0
served_B = 0
print(f"{'req':<5}{'kind':<14}{'esperado(ms)':<14}{'servido por':<13}resultado")
print("-" * 74)
for i, (kind, dur) in enumerate(REQUESTS):
    try:
        if kind in ("down", "rate_limited"):
            call_model(kind, dur)            # lanza de inmediato
            waited = 0
            result = "ok"
            served = "modelo"
        elif dur > LATENCY_BUDGET_MS:
            waited = LATENCY_BUDGET_MS        # esperamos solo el presupuesto
            raise ModelTimeout(f"corte a {LATENCY_BUDGET_MS} (habria tardado {dur})")
        else:
            waited = dur
            call_model(kind, dur)
            result = "ok"
            served = "modelo"
    except ModelDown:
        waited = 0; result = "down"; served = "fallback"
    except RateLimited:
        waited = 0; result = "rate_limited"; served = "fallback"
    except ModelTimeout as e:
        result = str(e); served = "fallback"
    wait_B += waited
    worst_B = max(worst_B, waited)
    served_B += 1
    print(f"{i:<5}{kind:<14}{dur:<14}{served:<13}{result}")

print("-" * 74)
print(f"Todos respondieron: {served_B}/{len(REQUESTS)} (los fallidos, por fallback).")
print()
print("Espera total simulada (la suma de lo que el sistema estuvo bloqueado):")
print(f"  SIN timeout : {wait_A:>6} ms   (peor caso: {worst_A} ms bloqueado)")
print(f"  CON timeout : {wait_B:>6} ms   (peor caso: {worst_B} ms bloqueado)")
print(f"  el timeout recorto {wait_A - wait_B} ms de espera y acoto el peor "
      f"caso de {worst_A} a {worst_B} ms.")

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

req  kind          esperado(ms)  servido por  resultado
--------------------------------------------------------------------------
0    ok            120           modelo       ok
1    ok            340           modelo       ok
2    slow          30000         fallback     corte a 800 (habria tardado 30000)
3    rate_limited  0             fallback     rate_limited
4    ok            210           modelo       ok
5    slow          2500          fallback     corte a 800 (habria tardado 2500)
6    down          0             fallback     down
7    ok            180           modelo       ok
8    ok            260           modelo       ok
9    slow          15000         fallback     corte a 800 (habria tardado 15000)
--------------------------------------------------------------------------
Todos respondieron: 10/10 (los fallidos, por fallback).

Espera total simulada (la suma de lo que el sistema estuvo bloqueado):
  SIN timeout :  48610 ms   (peor caso: 30000 ms bloqueado)
  CON timeout :   3510 ms   (peor caso: 800 ms bloqueado)
  el timeout recorto 45100 ms de espera y acoto el peor caso de 30000 a 800 ms.

Lee los números, porque el efecto del timeout es dramático y concreto.

La espera total: de 48.6 segundos a 3.5 segundos. Sin timeout, el sistema estuvo bloqueado 48610 ms —casi 49 segundos— sumando las esperas de las diez llamadas, dominadas por los dos cuelgues (30000 + 15000 = 45 segundos solo en esos dos). Con timeout, la espera total bajó a 3510 ms. El timeout recortó 45100 ms de espera inútil: los segundos que el sistema habría pasado con el teléfono pegado a la oreja escuchando el tono. Ese tiempo no es abstracto: es tiempo en que hilos del servidor están bloqueados sin atender a nadie más.

El peor caso: de 30 segundos a 800 ms. Igual de importante que el total es el peor caso individual. Sin timeout, un solo request malo (el cuelgue de 30 s) bloqueó al sistema medio minuto —imagina a un cliente de Mercado esperando medio minuto a que cargue una búsqueda—. Con timeout, ningún request bloqueó más de 800 ms, el presupuesto. El timeout no solo baja el promedio; acota el máximo, que es lo que evita que un cuelgue puntual arrastre la experiencia de todos.

Y todos respondieron. Mira la última columna de la tabla: los cuatro requests que fallaron (los dos cuelgues, el down, el rate_limited) se sirvieron por fallback. El sistema no se cayó en ninguno; cortó la espera y respondió con la ruta determinista. La tabla te muestra el patrón completo por request: los ok los sirvió el modelo (esperando su duración real, todas bajo el presupuesto), y los fallidos los sirvió el fallback tras cortar. 10 de 10 respondieron. El timeout hizo posible el fallback: sin cortar la espera, el sistema seguiría colgado en el request 2 y nunca habría llegado al fallback.

La implicación arquitectónica: el timeout es la pieza que convierte un cuelgue en algo manejable. Un cuelgue, por sí solo, no es ni éxito ni error: es una espera que no termina, y contra eso ninguna otra defensa sirve —el fallback no se dispara porque no hay excepción, el breaker no cuenta un fallo porque no hay fallo, solo espera—. El timeout le pone un límite a la espera y, al vencerse, lanza un ModelTimeout que sí es un fallo ruidoso, y entonces el fallback reacciona. Por eso el timeout va primero.

Profundización: por qué una API de modelo es una dependencia especialmente frágil

Los tres fallos de disponibilidad, con su matiz de IA. Los tres se parecen a los de cualquier dependencia de red, pero cada uno tiene un peso distinto cuando la dependencia es un modelo:

  • Caído (5xx / sin respuesta). El proveedor tiene una interrupción, o la red entre tú y él falla. Igual que cualquier servicio caído. La defensa: timeout + fallback + (si se repite) circuit breaker.
  • Lento. Aquí está el matiz grande: para un LLM, la lentitud es el caso normal, no la excepción. Como viste en el módulo 2, una llamada al modelo tarda de cientos de milisegundos a varios segundos cuando todo va bien. Eso significa que tu presupuesto de latencia y tu timeout no son un seguro contra rarezas; son una restricción de primera clase que convive con la operación normal. Elegir el valor del timeout es un tradeoff real: muy corto y cortas llamadas legítimas que iban a contestar; muy largo y toleras cuelgues que degradan la experiencia. (La guía de resiliencia, M2, trata cómo elegir ese valor; a menudo se ancla en un percentil alto de la latencia observada más un margen.)
  • Rate-limited (429). Este es más central que en un servicio clásico. Los proveedores de modelos imponen cuotas estrictas —por tokens por minuto, por requests por minuto—, y cuando las cruzas, te niegan el servicio con un 429. Lo traicionero: el rate limit aparece justo cuando más tráfico tienes (un pico de ventas, una campaña), es decir, en el peor momento. Y —esto lo desarrolla la lección 6— reintentar ciegamente un 429 lo empeora: cada reintento cuenta contra la cuota y prolonga el bloqueo. Por eso el rate limit no se resuelve reintentando; se resuelve dejando de llamar (breaker) y sirviendo el fallback.

El costo por llamada le da un peso extra al timeout. En un servicio clásico, esperar de más "solo" cuesta latencia. En un modelo, cada llamada que completa cuesta dinero (tokens). Esto tiene una consecuencia sutil: un timeout que corta una llamada que iba a completar te ahorra la espera, pero si el proveedor ya procesó los tokens, quizás ya pagaste. El diseño fino del timeout con modelos considera esto; para esta guía, basta con retener que el timeout protege tu latencia y tu disponibilidad, y que el manejo del costo es el tema del módulo 2 (presupuestos) y de la lección 6 (breaker).

El timeout infinito es un antipatrón, no un default inocente. Aquí hay una trampa de librería. Muchos clientes de API traen un timeout por defecto altísimo (30, 60 segundos) o directamente ninguno. Un desarrollador que no configura el timeout explícitamente está eligiendo el timeout infinito sin saberlo, y hereda todos los cuelgues del proveedor como cuelgues propios. La regla dura: toda llamada a una dependencia externa lleva un timeout explícito, elegido a partir de tu presupuesto de latencia, no el default de la librería. En un componente de IA esto es aún más importante porque la latencia normal ya es alta y variable, así que la frontera entre "lento normal" y "colgado" hay que definirla a propósito.

El timeout habilita a las otras defensas. Retén la cadena de dependencia, porque ordena el módulo:

   llamada al modelo
        │
        ▼
   ┌──────────┐  vence el limite   ┌──────────────┐
   │ TIMEOUT  │──────────────────► │ ModelTimeout │  (ahora es RUIDOSO)
   │ (L4)     │                    │ (excepcion)  │
   └──────────┘                    └──────────────┘
                                         │
                          ┌──────────────┼──────────────┐
                          ▼              ▼               ▼
                     FALLBACK (L5)  cuenta para    (si se repite)
                     responde con   el breaker      abre el breaker (L6)
                     ruta alterna   (L6)

El timeout es la base; el fallback y el breaker se construyen encima. Sin timeout, un cuelgue nunca se convierte en excepción, y ni el fallback ni el breaker se enteran de que hubo un problema. Por eso esta lección va antes que las otras dos.

Errores comunes

Timeout infinito (o el default de la librería). Qué pasa: el equipo llama al modelo sin configurar el timeout, hereda el default de 60 segundos del cliente HTTP, y el día que el proveedor se cuelga, cada request de búsqueda de Mercado se congela un minuto —los hilos se agotan y el servicio entero se degrada—. Por qué pasa: el timeout no se ve en desarrollo (el modelo siempre contestó rápido), así que nadie lo configuró, y el default es peligrosamente alto o inexistente. Cómo detectarlo: busca en tu código la llamada al modelo; si no hay un timeout= explícito anclado en tu presupuesto, tienes el antipatrón. Cómo corregirlo: pon un timeout explícito, derivado de tu presupuesto de latencia (módulo 2), no del default. El ejemplo lo mide: sin timeout, 48 s de espera; con timeout de 800 ms, 3.5 s.

Confundir "lento" con "roto" y matar llamadas legítimas. Qué pasa: el equipo, escarmentado por los cuelgues, pone un timeout agresivo de 200 ms —pero la latencia normal del modelo es de 400-600 ms—, así que corta la mayoría de las llamadas buenas y sirve fallback casi siempre, degradando la calidad sin necesidad. Por qué pasa: se elige el timeout sin mirar la distribución real de latencia del modelo, olvidando que para un LLM lo lento es normal. Cómo detectarlo: tu tasa de timeouts es alta incluso cuando el proveedor está sano. Cómo corregirlo: ancla el timeout en un percentil alto de la latencia observada (p95/p99) más un margen, no en un número al azar; el timeout debe distinguir "colgado" de "lento normal". La guía de resiliencia (M2) trata cómo elegir ese valor.

Reintentar un rate limit inmediatamente. Qué pasa: la API devuelve 429, y el código reintenta de inmediato, y otra vez, y otra —cada reintento cuenta contra la cuota agotada y prolonga el bloqueo, convirtiendo un rate limit corto en una tormenta de reintentos que se autoperpetúa—. Por qué pasa: se trata el 429 como un error transitorio cualquiera ("reintentar suele funcionar"), sin ver que este error se empeora al reintentar. Cómo detectarlo: tu manejo del 429 es un reintento inmediato sin backoff ni breaker. Cómo corregirlo: ante un 429, no reintentes de inmediato —usa backoff (guía de resiliencia M3) y, si persiste, abre el circuit breaker y sirve el fallback (lección 6)—. El rate limit se resuelve dejando de llamar, no llamando más.

Ejercicios

Ejercicio 1 — Elige el timeout. El presupuesto de latencia de la búsqueda de Mercado es de 1 segundo total (módulo 2), del cual el resto del pipeline (parsing, ranking, render) consume ~300 ms, dejando ~700 ms para la llamada al modelo. La latencia del modelo, medida en producción, es: p50 = 250 ms, p95 = 600 ms, p99 = 1200 ms. Propón un valor de timeout para la llamada al modelo y justifícalo. ¿Qué pasa con las llamadas del p99?

Ver solución

Un timeout razonable sería ~700 ms —el presupuesto disponible para la llamada—, que además queda por encima del p95 (600 ms). Con eso, cortas por debajo de tu presupuesto total (respetas el SLA de 1 s), y dejas pasar el 95% de las llamadas legítimas (las que tardan hasta 600 ms).

Las llamadas del p99 (1200 ms) se cortarían: exceden el presupuesto de 700 ms, así que se convierten en ModelTimeout y se sirven por fallback. Y eso está bien: son llamadas que, de completarse, romperían tu presupuesto de latencia total (1200 ms de modelo + 300 ms de pipeline = 1500 ms, por encima del segundo prometido). Es preferible servir un fallback rápido que hacer esperar al cliente 1.5 s. El ~1% del p99 se degrada a cambio de proteger el presupuesto del 99% restante.

El tradeoff: si subieras el timeout a 1200 ms para "alcanzar" al p99, dejarías de cortar esas llamadas pero romperías el presupuesto total y harías esperar a todos más. Si lo bajaras a 300 ms, cortarías gran parte del p95 legítimo y servirías fallback de más. ~700 ms equilibra: respeta el presupuesto y salva la gran mayoría de las llamadas buenas. (La guía de resiliencia, M2, formaliza esta elección.)

Ejercicio 2 — Por qué el timeout va antes que el fallback. Un compañero propone: "pongamos el fallback y ya; si el modelo falla, caemos a keywords, no necesitamos timeout". Explica con el ejemplo de esta lección por qué el fallback sin timeout no protege contra un cuelgue, y qué pasaría exactamente en el request 2 (el cuelgue de 30 s) con fallback pero sin timeout.

Ver solución

El fallback se dispara cuando la llamada al modelo lanza una excepción (o devuelve un error). Pero un cuelgue no lanza nada: la llamada simplemente no regresa, se queda esperando. El fallback está escrito como un except, y ese except nunca se activa porque no hay excepción —solo hay espera—.

En el request 2 (el cuelgue de 30 s) con fallback pero sin timeout: la llamada call_model se quedaría esperando la respuesta del modelo indefinidamente (en el mundo real, hasta el timeout del socket, que puede ser minutos, o para siempre). El hilo que atiende ese request se queda bloqueado. El bloque except que llevaría al fallback nunca se ejecuta, porque no se lanzó ninguna excepción: no hubo un error, hubo una espera. El cliente ve la búsqueda congelada; el hilo no atiende a nadie más.

El timeout es lo que arregla esto: al vencerse los 800 ms, lanza un ModelTimeout —convierte la espera silenciosa en un fallo ruidoso—, y entonces el except ModelTimeout se activa y el fallback responde. Por eso el orden es timeout → fallback: el timeout crea la excepción que el fallback necesita para reaccionar. El fallback sin timeout es un extintor sin alguien que jale la alarma: está ahí, pero nada lo dispara.

Ejercicio 3 — Los tres fallos y su respuesta. Para cada uno de los tres fallos de disponibilidad, di (a) cómo se manifiesta la llamada (qué devuelve o hace la API), (b) por qué reintentar de inmediato es buena o mala idea, y (c) la defensa de este módulo: (1) el modelo está caído (503); (2) el modelo está rate-limited (429); (3) el modelo está lento (tarda 8 s).

Ver solución
  • (1) Caído (503): (a) la API devuelve un error 503 explícito de inmediato. (b) Reintentar puede ayudar si la caída es un blip transitorio (un reintento con backoff a veces cae en un momento donde el servicio ya se recuperó), pero si la caída es prolongada, reintentar solo suma latencia. (c) Defensa: fallback de inmediato para responder, y circuit breaker si se repite para dejar de intentar (lección 6). Un reintento con backoff es aceptable (guía de resiliencia M3), pero no bloquees al cliente esperándolo: sírvele el fallback.
  • (2) Rate-limited (429): (a) la API devuelve 429 de inmediato. (b) Reintentar de inmediato es mala idea: cada reintento cuenta contra la cuota agotada y prolonga el bloqueo. (c) Defensa: no reintentar de inmediato; abrir el circuit breaker y servir el fallback hasta que la ventana de cuota se libere (lección 6). El rate limit se resuelve dejando de llamar.
  • (3) Lento (8 s): (a) la llamada no devuelve nada; se queda esperando (cuelgue). (b) "Reintentar" ni siquiera aplica hasta que decidas dejar de esperar; por sí solo, esperas 8 s. (c) Defensa: timeout —cortas a tu presupuesto (p.ej. 800 ms), lo conviertes en ModelTimeout, y caes al fallback—. Esta lección. El timeout es la defensa específica de la lentitud, porque es el único fallo que no se anuncia solo.

Resumen y siguiente paso

En esta lección instalaste la primera defensa de la familia de disponibilidad: el timeout —no esperar para siempre a una API que se colgó—, y entendiste por qué depender de una API de modelo lenta y con cuotas es una dependencia especialmente frágil. Lo viste con el teléfono que suena y suena mientras tu día se detiene, y lo mediste: con un timeout de 800 ms, la espera total del sistema bajó de 48610 ms a 3510 ms, el peor caso de 30000 ms a 800 ms, y las 10 llamadas respondieron —las fallidas, por fallback—. Y viste la pieza clave de diseño: el timeout convierte un cuelgue silencioso en un fallo ruidoso que el fallback y el breaker pueden manejar, por eso va primero. Retuviste los tres fallos de disponibilidad —caído, lento, rate-limited— con su matiz de IA: para un modelo, lo lento es normal, el rate limit es central y traicionero, y reintentar un 429 lo empeora.

Antes de avanzar deberías poder: explicar por qué el timeout infinito (o el default de la librería) es un antipatrón; elegir un valor de timeout a partir del presupuesto de latencia; argumentar por qué el fallback sin timeout no protege contra un cuelgue; y distinguir la respuesta correcta a un caído, a un rate-limited y a un lento. La mecánica del timeout a fondo, recuerda, vive en resilience-and-reliability-patterns-guide M2.

La lección 5 toma la ruta que el timeout habilita y la desarrolla: el fallback y la degradación elegante. Vas a ver, ejecutado, la búsqueda semántica de Mercado cayendo a una cascada —semántica → cache → keywords— cuando el modelo falla, con la disponibilidad subiendo de 70% a 100% y las respuestas degradadas marcadas como lo que son: peores, pero válidas. La forma de que "el modelo cayó" deje de significar "el sistema cayó" y pase a significar "el sistema respondió un poco peor".

Recursos

  • resilience-and-reliability-patterns-guide (este ecosistema), M2 "Timeouts" — la referencia central para la mecánica del timeout que aquí solo aplicamos: cómo implementarlo con hilos o async, cómo elegir el valor a partir de percentiles de latencia, cómo interactúa con reintentos y con el presupuesto total. En español.
  • Anthropic, documentación de Claude — docs.anthropic.com. Consulta las páginas de rate limits (cuotas por tokens y requests, el 429) y de errores para entender los modos de fallo de disponibilidad de un modelo servido por API, a nivel conceptual y sin fijar versión. En inglés.
  • architecture-for-ai-native-systems-guide, Módulo 2 (latencia y costo como arquitectura) — el presupuesto de latencia del que se deriva el valor del timeout, y por qué la lentitud del modelo es una restricción de primera clase. En español.
  • Chip Huyen, AI Engineering (O'Reilly, 2024). Los capítulos sobre latencia, costo y confiabilidad de inferencia tratan la dependencia de una API de modelo con sus cuotas y su latencia variable como una propiedad de diseño. En inglés.