Cómo Construir Pipelines Confiables sobre APIs de Terceros
Las APIs de terceros forman parte de tu sistema aunque no las controles. Pueden volverse lentas, aplicar rate limits, entregar datos viejos o dejar de responder. Una integración confiable no es solamente un request HTTP que casi siempre funciona: es un límite que absorbe fallas externas sin propagarlas al resto de la aplicación.
Tratar al proveedor como una dependencia no confiable
La primera suposición útil es que toda API externa eventualmente va a fallar de una forma que el happy path no contemplaba. Eso no significa que el proveedor sea malo. Fallan las redes, se agotan cuotas, hay mantenimiento y cambian los tiempos de respuesta.
El límite de integración debería definir qué ocurre cuando el proveedor está lento, no disponible, devuelve datos parcialmente incorrectos o aplica un rate limit temporal. La confiabilidad empieza cuando esos casos son explícitos y no simples excepciones genéricas.
Separar adaptadores de proveedor del comportamiento de la aplicación
El código de negocio no debería conocer el formato de respuesta, las particularidades de autenticación o el vocabulario de errores de cada servicio externo. Prefiero un adaptador por proveedor que traduzca cada API a un contrato interno.
Eso permite cambiar proveedores, combinar varias fuentes o desactivar una temporalmente sin reescribir el resto de la aplicación. La evaluación de pronósticos, alertas y persistencia puede trabajar con un modelo normalizado mientras lo específico de cada proveedor queda en el borde.
interface WeatherProvider {
getForecast(input: ForecastRequest): Promise<NormalizedForecast>;
}Usar timeouts antes de pensar en reintentos
Un request que no falla pero tarda demasiado puede ser más dañino que un error rápido. Toda llamada externa debería tener un tiempo máximo durante el cual puede ocupar un worker o un slot de un job programado.
Los reintentos sólo tienen sentido cuando el primer intento tiene un timeout claro. De lo contrario, varias llamadas lentas pueden superponerse y amplificar el problema original.
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8_000);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}Reintentar sólo fallas que pueden mejorar con el tiempo
No todos los errores merecen un retry. Una falla temporal de red o una respuesta 5xx puede funcionar en un intento posterior. Credenciales inválidas, input incorrecto o una cuota agotada normalmente no.
Los reintentos deben ser limitados y espaciados, preferentemente con exponential backoff y jitter para evitar que varias instancias vuelvan a intentar exactamente al mismo tiempo. El objetivo es recuperarse, no insistir a cualquier costo.
delay = min(base * 2 ** attempt + jitter, maxDelay)Los circuit breakers detienen fallas repetidas
Cuando un proveedor ya está fallando o limitando tráfico, llamarlo nuevamente en cada ejecución programada genera más carga y desperdicia capacidad. Un circuit breaker registra que las fallas recientes superaron un umbral y detiene temporalmente nuevos intentos.
El tiempo de apertura debería reflejar el tipo de falla. Un cooldown corto puede alcanzar para respuestas 429 transitorias, mientras que un error explícito de cuota diaria puede justificar mantener el circuito abierto mucho más tiempo. Después del cooldown, una prueba limitada puede decidir si vuelve el tráfico normal.
CLOSED -> demasiadas fallas -> OPEN
OPEN -> termina cooldown -> HALF_OPEN
HALF_OPEN -> éxito -> CLOSED
HALF_OPEN -> falla -> OPENLa vigencia puede valer más que un request nuevo
Un pipeline confiable debería saber qué tan viejos son sus datos actuales antes de volver a consultar un proveedor. Si un pronóstico todavía es suficientemente reciente para el comportamiento del producto, reutilizarlo puede ser mejor que hacer otro request externo.
La vigencia es una decisión de producto, no solamente un TTL de cache. Distintos consumidores pueden tolerar edades diferentes: un pronóstico interactivo, la evaluación de alertas y un resumen diario pueden tener ventanas distintas.
Reutilizar una respuesta entre varios consumidores
Los requests duplicados suelen aparecer porque distintas partes de la aplicación piden de forma independiente los mismos datos. Un refresh programado puede consultar un proveedor y minutos después un proceso de alertas o comparación vuelve a pedir exactamente lo mismo.
Cuando sea posible, conviene consultar una vez, normalizar una vez y persistir o compartir el resultado para que pronósticos, comparaciones, alertas y resúmenes reutilicen el mismo snapshot. Eso reduce consumo de cuota y hace que esas funciones razonen sobre los mismos datos.
Hacer idempotente la persistencia
Los reintentos y las invocaciones duplicadas son inevitables en alguna capa, por lo que escribir dos veces el mismo resultado debería ser seguro. La idempotencia puede lograrse con claves únicas, upserts, tokens de deduplicación o comparando el snapshot normalizado nuevo con el actual antes de escribir.
En pipelines de datos prefiero no generar un nuevo evento o snapshot cuando nada relevante cambió. Eso reduce crecimiento de la base y hace más fácil interpretar la actividad posterior.
if (sameMeaningfulData(previous, incoming)) {
return previous;
}
return persist(incoming);Los fallbacks necesitan semántica explícita
Tener varios proveedores puede mejorar la disponibilidad, pero sólo si el sistema sabe qué significa usar un fallback. El proveedor B puede tener otra resolución, frecuencia de actualización o conjunto de campos que el proveedor A.
Hay que normalizar lo que realmente es comparable, conservar metadata de la fuente y evitar fingir que todos los proveedores son intercambiables. La aplicación debería saber si está sirviendo datos primarios, datos de fallback o el último valor válido conocido.
Los pipelines programados también necesitan coordinación
Los reintentos, circuit breakers y controles de vigencia no impiden que dos instancias de la aplicación comiencen el mismo job programado al mismo tiempo. Ese es otro problema de coordinación.
En Vienor uso advisory locks de PostgreSQL para que el trabajo programado tenga un único dueño entre instancias sin bloquear. El lock protege la ejecución concurrente, mientras que los controles del pipeline manejan fallas externas y efectos duplicados.
Observar el límite, no sólo el error final
Un contador de errores no explica por qué una integración está degradada. Señales útiles son latencia, tasa de timeouts, status HTTP, cantidad de reintentos, estado del circuito, vigencia de datos, trabajo duplicado evitado y frecuencia de uso de fallbacks.
Esas métricas permiten distinguir una regresión propia de un incidente del proveedor y ajustar políticas de retry o vigencia usando comportamiento real de producción en lugar de suposiciones.
El principio de diseño
Una API externa debe tratarse como una fuente de datos y disponibilidad inciertos, no como una función local. La capa de integración es la responsable de absorber esa incertidumbre.
Limitá la duración de las llamadas, reintentá selectivamente, dejá de llamar cuando la falla ya es conocida, reutilizá resultados vigentes, hacé idempotentes las escrituras y mantené los detalles de cada proveedor fuera del resto del sistema. Así una integración deja de ser una dependencia frágil y pasa a ser un límite controlado.
Trabajo relacionado
Ver cómo se usan estos controles de confiabilidad en el pipeline meteorológico de Vienor