Módulo 7: Reliability Patterns & Production Checklist

1. Introducción: Reliability Patterns

Descripción

"Funciona en mi máquina" no significa "funciona en producción." En producción las cosas fallan — no es una posibilidad, es una certeza. La pregunta no es si OpenAI va a devolverte un error 429, sino cuándo. Los reliability patterns no previenen esos fallos: te permiten manejarlos de forma que tu servicio siga funcionando o falle de manera controlada. En este módulo vas a construir esa capa de resiliencia sobre la clean architecture que implementaste en el Módulo 6.

Al terminar este módulo, vas a tener:

  • Un sistema que reintenta errores transitorios sin saturar la API
  • Un circuit breaker que detecta outages y deja de enviar requests inútiles
  • Rate limiting del lado del cliente para controlar tu gasto y tráfico
  • Una cadena de fallbacks que mantiene el servicio vivo incluso cuando el provider primario cae
  • Health checks reales que Kubernetes puede usar para decidir si tu pod está listo

Los failure modes reales de LLM APIs

Antes de escribir código, necesitas entender los escenarios que vas a enfrentar. Estos no son hipotéticos — son situaciones que ocurren regularmente en producción:

Escenario 1: Rate limit bajo presión
  - Tu app tiene 100 usuarios activos simultáneos
  - Todos generan requests al mismo tiempo (lunes 9am)
  - OpenAI te devuelve 429 (Too Many Requests)
  - Sin retry: todos ven error 500
  - Con retry + backoff: los requests se distribuyen, la mayoría completa

Escenario 2: Timeout en prompt largo
  - Usuario envía un documento de 50 páginas
  - El modelo tarda 45s en procesarlo
  - Tu client tiene timeout de 30s
  - Sin timeout handling: excepción no capturada, error 500
  - Con timeout retry: reintenta con prompt truncado

Escenario 3: Outage de 2 horas
  - OpenAI tiene un incidente de infraestructura
  - Sin circuit breaker: tu app envía 5000 requests fallidos en 2 horas
    (cada request hace 3 retries = 15000 llamadas inútiles a OpenAI)
    (costo: tu servidor procesando requests que nunca van a funcionar)
  - Con circuit breaker: después de 5 failures consecutivos, el circuit abre
    Las siguientes llamadas fallan inmediatamente (sin llamar a OpenAI)
    Cuando pasan 60s, el circuit prueba una llamada → si funciona, cierra

Escenario 4: Spike de tráfico
  - Tu blog post se vuelve viral, 1000 usuarios en 10 minutos
  - Sin rate limiting: tus 1000 requests simultáneos todos fallan con 429
  - Con rate limiter: requests se throttlean, se procesan 8/segundo
    Los usuarios esperan un poco, pero obtienen respuesta

Escenario 5: Respuesta malformada
  - El LLM devuelve texto en vez de JSON (por algún motivo)
  - Sin fallback: excepción de parse, error 500
  - Con fallback: intenta parsear, si falla devuelve respuesta por defecto
    Y logea para investigar

Los 5 patterns del módulo

┌──────────────────────────────────────────────────────────────┐
│             RELIABILITY LAYER                                │
│                                                              │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────────┐  │
│  │   RETRY     │  │   CIRCUIT    │  │   RATE LIMITING    │  │
│  │  + BACKOFF  │  │   BREAKER    │  │  (Token Bucket)    │  │
│  │             │  │              │  │                    │  │
│  │ Transitorio │  │  Persistente │  │   Throttling       │  │
│  │ → reintenta │  │  → falla     │  │   → controla       │  │
│  │   con espera│  │    rápido    │  │     velocidad      │  │
│  └─────────────┘  └──────────────┘  └────────────────────┘  │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │              FALLBACK CHAIN                             │  │
│  │                                                         │  │
│  │  Primary → Secondary Model → Cached Response → Static  │  │
│  └─────────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐  │
│  │              HEALTH CHECKS                              │  │
│  │  Readiness + Liveness + Dependency checks reales        │  │
│  └─────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Por qué la clean architecture del M6 hace esto fácil

# ANTES del M6 (sin clean architecture):
# Si quisieras añadir retry a tu app, tendrías que modificar:
# - La god function en main.py (donde está el LLM call mezclado con todo)
# - Cada lugar donde se llama al LLM
# - Los tests (que dependen de la implementación interna)

# DESPUÉS del M6 (con clean architecture y DI):
# Para añadir reliability, solo creas wrappers del LLMProvider:

from src.infrastructure.llm_provider import LLMProvider

class RetryProvider:
    """Wrappea cualquier LLMProvider con retry logic."""
    def __init__(self, inner: LLMProvider, max_attempts: int = 3):
        self._inner = inner
        self._max = max_attempts
    
    def complete(self, messages: list[dict], **kwargs) -> str:
        # Añade retry sin modificar domain ni processing
        ...

class CircuitBreakerProvider:
    """Wrappea cualquier LLMProvider con circuit breaker."""
    def __init__(self, inner: LLMProvider, failure_threshold: int = 5):
        self._inner = inner
        ...
    
    def complete(self, messages: list[dict], **kwargs) -> str:
        ...

# En dependencies.py (el único lugar que cambia):
def get_llm_provider() -> LLMProvider:
    base = OpenAIProvider.from_settings(settings)
    with_retry = RetryProvider(base, max_attempts=3)
    with_circuit_breaker = CircuitBreakerProvider(with_retry, failure_threshold=5)
    return with_circuit_breaker

# El domain (sentiment_service.py) no cambia.
# Los tests del domain no cambian.
# Solo dependencies.py cambia.

Cuándo usar cada pattern

¿El error es transitorio (dura segundos)?
└─ SÍ → RETRY con backoff
   Ejemplos: timeout momentáneo, rate limit que se libera pronto

¿El servicio ha fallado muchas veces seguidas?
└─ SÍ → CIRCUIT BREAKER
   Ejemplos: outage, servidor caído, rate limit prolongado

¿El tráfico puede superar los límites de la API?
└─ SÍ → RATE LIMITING
   Ejemplos: spikes de tráfico, carga sostenida alta

¿El primary provider falla y necesitas seguir sirviendo?
└─ SÍ → FALLBACK
   Ejemplos: outage de OpenAI, modelo no disponible

¿Necesitas saber si el sistema puede recibir tráfico?
└─ SÍ → HEALTH CHECKS
   Ejemplos: Kubernetes readiness, monitoring, alertas

Composición: los patterns trabajan juntos

# La secuencia correcta de composición:

# 1. Rate Limiter: ¿tengo capacidad para este request?
#    Si NO → rechazar con 503, no gastar recursos
#    Si SÍ → continuar

# 2. Circuit Breaker: ¿el servicio está disponible?
#    Si OPEN → ir al fallback inmediatamente
#    Si CLOSED/HALF-OPEN → continuar

# 3. Retry: ¿el request falló por error transitorio?
#    Si SÍ → reintenta con backoff
#    Si NO (error permanente) → propagar error

# 4. Fallback: ¿todos los retries fallaron?
#    PRIMARY → SECONDARY → CACHED → STATIC DEFAULT

# En código:
def get_llm_provider() -> LLMProvider:
    base = OpenAIProvider.from_settings(settings)  # La llamada real
    
    # Aplicar en el orden correcto (de afuera hacia adentro):
    with_retry = RetryProvider(base, max_attempts=3)           # Innermost
    with_cb = CircuitBreakerProvider(with_retry, threshold=5)  # Wraps retry
    with_fallback = FallbackProvider(with_cb, fallback_model=secondary)  # Outermost
    
    return with_fallback

# Flujo de una request:
# FallbackProvider.complete()
#   → CircuitBreakerProvider.complete()
#     → [Circuit CLOSED: pasa]
#     → RetryProvider.complete()
#       → Intento 1: TimeoutError → esperar 1s
#       → Intento 2: TimeoutError → esperar 2s  
#       → Intento 3: success → retornar
#   → [Después de 5 failures del circuit, abre]
#   → FallbackProvider detecta CircuitOpenError
#   → Intenta secondary_provider
#   → Secondary funciona → retornar con flag degraded=True

Prerequisitos del módulo

# Instalar las dependencias de reliability
pip install tenacity     # Retry con backoff
pip install pybreaker    # Circuit breaker (opcional, también hay implementación custom)

Roadmap del módulo

#CápsulaPatternQué hace
01IntroducciónLos 5 patterns y composición
02Error Handling LLM APIsBaseClasificar y manejar cada tipo de error
03Retry y backoffRetrytenacity con exponential backoff + jitter
04Circuit BreakersCircuitEstados, thresholds, auto-recovery
05Rate LimitingRateToken bucket, throttling, burst control
06Fallbacks y Health ChecksFallbackFallback chain + health endpoints reales
07Proyecto Reliability LayerTodosIntegración completa con clean architecture
08Resumen y TroubleshootingProduction checklist

Ejercicios

Ejercicio 1: Clasifica los failure modes

Para cada escenario, indica el pattern correcto:

  1. OpenAI devuelve 429 después de un spike de tráfico
  2. La app lleva 10 minutos recibiendo errors 500 de OpenAI
  3. 200 usuarios hacen requests simultáneos a un endpoint con límite de 50/min
  4. El LLM devuelve JSON malformado en el 2% de los requests
Ver solución
  1. 429 después de spike → Retry con exponential backoff (error transitorio que se resuelve solo)
  2. 10 minutos de errors 500 → Circuit Breaker (error persistente, dejar de intentar, abrir el circuit)
  3. 200 usuarios, límite 50/min → Rate Limiting (throttling para no superar el límite)
  4. JSON malformado → Fallback (error de parsing → respuesta por defecto + log para investigar)

Ejercicio 2: Diseñar la composición

Para una app con estas características:

  • Límite de OpenAI: 100 RPM
  • Se espera que OpenAI tenga ~99.5% uptime (outage ~4h/mes)
  • El LLM tiene ~1% de respuestas malformadas
  • Tienes un modelo de backup más barato

Diseña la cadena de composición de providers:

Ver guía
# De inner a outer:
primary = OpenAIProvider(model="gpt-4o", ...)
with_retry = RetryProvider(primary, max_attempts=3)  # Para errores transitorios
with_cb = CircuitBreakerProvider(with_retry, failure_threshold=5, recovery_timeout=60)
                                            # Para los outages de 4h
rate_limiter = RateLimitedProvider(with_cb, rpm=80)  # 80% del límite de 100 RPM
fallback = FallbackProvider(
    rate_limiter,
    secondary=OpenAIProvider(model="gpt-4o-mini", ...),  # Backup más barato
    static_default={"sentiment": "unknown", "score": 0.0, "confidence": 0.0}
)

Ejercicio 3: Identificar el pattern incorrecto

En cada caso, alguien eligió el pattern equivocado. Explica por qué está mal y cuál usarías tú:

  1. Un dev puso retry con 10 intentos para un AuthenticationError (401)
  2. Un dev puso circuit breaker con failure_threshold=1 para un LLM de análisis de sentimiento
  3. Un dev eliminó el rate limiter porque "ya OpenAI me limita"
Ver solución
  1. Retry para AuthError: un 401 significa que tu API key está mal. No importa cuántas veces lo reintentes — va a dar el mismo error. La acción correcta es no retry + alerta de config (log CRITICAL).
  2. Threshold=1: un solo failure abre el circuit. Esto causa falsos positivos constantes — cualquier timeout aislado va a activar el circuit. Threshold recomendado: 5-7 para servicios no críticos.
  3. Sin rate limiter: que OpenAI te limite significa que recibes 429s. Esos 429s activan retries, que consumen más recursos. El rate limiter del cliente previene que envíes requests que sabes que van a fallar. Te ahorras latencia, costo y carga en tu servidor.

Ejercicio 4: Calcular el impacto de un outage

Tu app tiene 50 usuarios/minuto. OpenAI tiene un outage de 15 minutos. Calcula para cada escenario:

MétricaSin reliabilityCon retry (3 intentos)Con retry + circuit breaker
Requests a OpenAI???
Tiempo de respuesta promedio???
Usuarios que ven error???
Ver solución
MétricaSin reliabilityCon retry (3 intentos)Con retry + CB (threshold=5)
Requests a OpenAI50 × 15 = 750750 × 3 = 2,250~15 (los primeros 5 fallos) + ~15 probes
Tiempo de respuesta~30s (timeout)~45s (3 timeouts)<1ms (circuit open, fallback)
Usuarios con error750 (todos)750 (todos, más lento)~5 (los que triggerearon el circuit)

El retry sin circuit breaker empeora la situación: triplica los requests fallidos y la latencia. El circuit breaker detiene el sangrado después de los primeros 5 failures.


Troubleshooting

"No sé si mi error es transitorio o permanente"

Aplica esta regla: si envías el mismo request 5 minutos después y podría funcionar → es transitorio. Si envías el mismo request 100 veces y siempre falla → es permanente. Los timeouts y 429s son transitorios; los 401 y 400 son permanentes.

"¿Necesito implementar todos los patterns?"

No necesariamente todos desde el día 1. La prioridad mínima para producción es:

  1. Error classification (siempre — sin esto no puedes decidir nada)
  2. Retry con backoff (casi siempre — protege contra transitorios)
  3. Health checks (siempre — Kubernetes los necesita)
  4. Circuit breaker (cuando esperas outages frecuentes)
  5. Rate limiting + Fallback (cuando tienes tráfico alto o modelos de backup)

"¿Cómo pruebo todo esto si no puedo forzar errores de OpenAI?"

Usa mocks. El MockProvider del M6 te permite simular cualquier error:

from src.infrastructure.llm_provider import LLMProviderError
from src.infrastructure.error_classifier import ErrorCategory

def create_failing_provider(error_type: ErrorCategory):
    """Crea un provider que falla con el tipo de error que necesitas testar."""
    mock = MockProvider()
    original = mock.complete
    def failing(messages, **kwargs):
        raise LLMProviderError(
            "Simulated error",
            category=error_type,
            should_retry=(error_type == ErrorCategory.TRANSIENT)
        )
    mock.complete = failing
    return mock

"¿El orden de composición importa?"

Sí, importa mucho. El orden correcto es: FallbackProvider(RateLimiter(CircuitBreaker(Retry(Base)))). Si inviertes el order de circuit breaker y retry, el retry anula el beneficio del circuit — sigue reintentando aunque el servicio esté caído. La cápsula 04 explica esto en detalle.


Resumen

  • Failure is not if, but when: los patterns no previenen fallos, los manejan
  • 5 patterns clave: retry (transitorios), circuit breaker (persistentes), rate limiting (throttling), fallback (degradación controlada), health checks (observabilidad)
  • La DI del M6 es la clave: wrappear providers sin tocar domain ni processing
  • Composición: los patterns se apilan, cada uno maneja su tipo de fallo

Recursos adicionales

  1. Release It! (Michael Nygard) — El libro de referencia para reliability patterns
  2. tenacity Documentation — La librería de retry
  3. Circuit Breaker Pattern (Martin Fowler) — El patrón explicado
  4. OpenAI Rate Limits — Los límites reales
  5. Designing Data-Intensive Applications (Kleppmann) — Reliability en sistemas distribuidos