Arquitecturas de datos: cómo implementar circuit breakers en pipelines que dependen de APIs externas
Arquitecturas de datos: cómo implementar circuit breakers en pipelines que dependen de APIs externas
El pipeline falla en silencio. No lanza una excepción visible, no activa ninguna alerta. Simplemente deja de procesar señales porque la API externa que alimenta la ingestión empezó a devolver 503 hace cuarenta minutos. El equipo lo descubre cuando el dashboard analítico lleva casi una hora sin actualizar datos.
Este escenario es más habitual de lo que los equipos quieren admitir. Cuando un pipeline depende de fuentes externas —APIs de terceros, endpoints de monitorización de menciones, feeds de señales públicas— la cadena de resiliencia interna deja de ser suficiente. Necesitas un mecanismo que detecte el fallo, deje de golpear el endpoint caído y se recupere de forma ordenada cuando el servicio vuelva. Ese mecanismo tiene nombre: circuit breaker.
Qué es un circuit breaker y por qué importa en ingestión de datos
El patrón circuit breaker nació en entornos de microservicios para proteger la estabilidad del sistema cuando un servicio dependiente falla. Aplicado a pipelines de datos, cumple la misma función: evitar que un endpoint degradado propague el error hacia los consumidores aguas abajo.
Un circuit breaker tiene tres estados:
- Cerrado (Closed): el tráfico fluye con normalidad. Se cuentan los fallos.
- Abierto (Open): se ha superado el umbral de fallos. Las llamadas se bloquean sin llegar al endpoint. El circuito permanece abierto durante un periodo de espera configurable.
- Semiabierto (Half-Open): transcurrido el periodo, se permiten unas pocas llamadas de prueba. Si tienen éxito, el circuito se cierra. Si no, vuelve a abrirse.
En un pipeline de ingestión que consume APIs externas, la ausencia de este patrón tiene consecuencias concretas: colas que se saturan, workers que bloquean hilos esperando timeouts de red, y —lo más peligroso— mensajes que se marcan como procesados aunque el dato nunca llegó.
Cómo definir los umbrales correctos
El error más habitual es configurar umbrales demasiado agresivos o demasiado permisivos. Un umbral demasiado bajo abre el circuito ante fallos transitorios normales (un pico de latencia puntual, un rate limit momentáneo). Uno demasiado alto deja al pipeline golpeando un endpoint caído durante demasiado tiempo.
Una aproximación práctica:
- Ventana de evaluación: define fallos dentro de una ventana deslizante, no de forma acumulativa. Por ejemplo: más del 50 % de errores en las últimas 20 llamadas.
- Tipos de error diferenciados: no todos los códigos de error merecen el mismo peso. Un
429 Too Many Requestsno es lo mismo que un500 Internal Server Error. El primero puede resolverse con backoff exponencial sin abrir el circuito; el segundo sí justifica escalar el contador de fallos. - Periodo de espera adaptativo: empieza con 30-60 segundos. Si el circuito vuelve a abrirse tras la primera prueba en semiabierto, dobla el periodo de espera. Este mecanismo —similar al backoff exponencial— reduce la presión sobre un endpoint que todavía no se ha recuperado del todo.
Implementación mínima viable en Python
Un equipo que empieza no necesita una librería de circuit breaking completa. Una implementación mínima puede controlarse con pocas decenas de líneas:
import time
from enum import Enum
class State(Enum):
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
class CircuitBreaker:
def __init__(self, failure_threshold=5, window=20, recovery_timeout=60):
self.failure_threshold = failure_threshold
self.window = window
self.recovery_timeout = recovery_timeout
self.state = State.CLOSED
self.failures = 0
self.last_failure_time = None
def call(self, func, *args, **kwargs):
if self.state == State.OPEN:
elapsed = time.time() - self.last_failure_time
if elapsed >= self.recovery_timeout:
self.state = State.HALF_OPEN
else:
raise Exception("Circuit OPEN — llamada bloqueada")
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise e
def _on_success(self):
self.failures = 0
self.state = State.CLOSED
def _on_failure(self):
self.failures += 1
self.last_failure_time = time.time()
if self.failures >= self.failure_threshold:
self.state = State.OPEN
Esta implementación no cubre ventana deslizante ni diferenciación de errores, pero es suficiente para validar el comportamiento en un entorno de staging antes de introducir una librería como pybreaker o resilience4j (en ecosistemas JVM).
Integración con la capa de observabilidad
Un circuit breaker sin telemetría es un mecanismo ciego. Necesitas saber cuándo se abre, cuánto tiempo permanece abierto y con qué frecuencia entra en estado semiabierto.
Los eventos mínimos que debes emitir como métricas o logs estructurados:
circuit_breaker.state_change→ con los camposfrom_state,to_state,endpoint,timestamp.circuit_breaker.call_blocked→ cada vez que se rechaza una llamada porque el circuito está abierto.circuit_breaker.recovery_attempt→ cuando el circuito pasa a semiabierto y lanza llamadas de prueba.
Con estos eventos en un sistema de observabilidad —Prometheus, Datadog, o cualquier stack compatible con OpenTelemetry— puedes construir alertas basadas en la frecuencia de aperturas del circuito, no solo en los errores directos del endpoint. Esto te da visibilidad anticipada: si el circuito empieza a abrirse con más frecuencia de lo habitual, el endpoint está degradándose antes de que colapse del todo.
Qué pasa con el dato perdido mientras el circuito está abierto
Este es el punto que los equipos postergan y que luego genera incidencias reales. Cuando el circuito está abierto, las llamadas no se realizan. El dato de ese intervalo no llega. La pregunta es: ¿ese dato se recupera después?
Depende de si la API externa soporta consultas por rango temporal. Si lo hace —como ocurre con APIs diseñadas para análisis de señales públicas, donde cada elemento tiene un timestamp de publicación verificable— puedes implementar un proceso de backfill controlado que, una vez que el circuito se cierra, relanza las consultas para cubrir el intervalo con el circuito abierto.
Cuando se trabaja con endpoints que exponen señales del universo público de Internet —menciones, tendencias, datos derivados de fuentes públicas— la capacidad de consultar por timestamp es precisamente lo que hace viable este patrón de recuperación. FeedScale, por ejemplo, expone sus endpoints con campos temporales precisos que permiten construir estas ventanas de backfill sin ambigüedad.
Si la API no soporta consultas retrospectivas, el dato perdido durante el periodo de apertura del circuito es irrecuperable. En ese caso, el equipo debe decidir cómo documentar ese gap en los metadatos del pipeline y cómo propagarlo a los consumidores aguas abajo para que no confundan ausencia de dato con ausencia de señal.
El circuit breaker no es una optimización de rendimiento. Es un mecanismo de integridad. Sin él, un endpoint externo degradado puede comprometer la consistencia de todo el pipeline sin que nadie lo detecte hasta que el daño ya está hecho. Implementarlo con umbrales bien calibrados y telemetría adecuada es una de las decisiones de arquitectura que más tiempo ahorra en entornos de producción reales.
Si tu pipeline consume APIs externas en producción y todavía no tienes este patrón implementado, ese es el desequilibrio que más vale corregir antes de añadir más fuentes de datos.