Ejecución Paralela en el Edge:
Self-Fetch Chain y Orquestación sin Límites
Cómo ejecutar pipelines de IA multi-agente que duran minutos en una plataforma diseñada para respuestas de milisegundos. El patrón que nos permitió escalar la orquestación de Synapse Studio sin abandonar el edge.
Cuando el Edge no Está Diseñado para Esperar
Las plataformas edge computing como Cloudflare Workers están optimizadas para respuestas rápidas: un request entra, se procesa en milisegundos, y sale. Pero ¿qué pasa cuando necesitas que varios agentes de IA colaboren durante 2-3 minutos en una misma tarea?
Los Workers tienen límites estrictos. El tiempo de CPU disponible es generoso en planes de pago, pero el wall-time — el reloj real entre que el request empieza y termina — es el verdadero cuello de botella. Si tu orquestación multi-agente necesita 3 pasos secuenciales de 60 segundos cada uno, ya estás fuera de los límites de un solo request.
El dilema del edge
Quieres las ventajas del edge computing — latencia mínima, distribución global, escalado automático — pero necesitas ejecutar procesos que duran órdenes de magnitud más que un request HTTP típico. La solución obvia es mover la orquestación a un servidor tradicional. Pero eso significa perder todo lo anterior.
La solución intuitiva — waitUntil() para trabajo en background — tiene sus propias limitaciones. En la práctica, el trabajo delegado vía waitUntil comparte el mismo contexto de ejecución, y en entornos con múltiples pasos encadenados, los timeouts y la estabilidad se degradan. Necesitábamos algo diferente.
Self-Fetch Chain: Cada Paso en su Propio Request
La idea central es simple: un Worker puede hacer fetch a sí mismo. En lugar de ejecutar todos los pasos de orquestación dentro de un solo request, cada paso se ejecuta en una invocación independiente que, al terminar, dispara la siguiente.
Request 1: Orquestación
Se recibe la tarea, se genera el plan multi-agente con el LLM orquestador, se crea el paso 1 y se ejecuta. Al terminar, hace fetch a /continue.
Request 2..N: Continuación
Cada /continue ejecuta el siguiente paso del plan. Lee el estado de la base de datos, identifica qué paso toca, lo ejecuta, y encadena al siguiente.
Request Final: Compilación
Cuando no quedan más pasos, se compilan los resultados de todos los agentes, se genera el output final y se actualiza el estado de la tarea.
Cada request tiene su propio presupuesto de CPU y wall-time completo. Un pipeline de 5 pasos que necesitaría 5 minutos seguidos ahora se ejecuta en 5 requests independientes de ~1 minuto cada uno. El edge ya no es un límite, es un multiplicador.
Flujo de una tarea de 4 pasos con 2 grupos paralelos
Promise.all ¿Por qué waitUntil en lugar de await?
El self-fetch se dispara vía waitUntil(fetch(...)), no con await. Esto permite que el request actual devuelva su respuesta inmediatamente ("paso 2 completado, quedan 2") mientras el siguiente request arranca en paralelo. El frontend recibe updates progresivos y el usuario no espera a que toda la cadena termine para ver progreso.
Grupos Paralelos: Misma Request, Múltiples Agentes
No todos los pasos necesitan ser secuenciales. Cuando el LLM orquestador planifica una tarea, asigna a cada paso un grupo paralelo (parallel_group). Los pasos dentro del mismo grupo se ejecutan concurrentemente; los grupos distintos se ejecutan secuencialmente.
{
"plan": [
{ "agent": "Analista", "role": "Investigar mercado", "group": 1 },
{ "agent": "Diseñador", "role": "Crear mockups", "group": 1 },
{ "agent": "Redactor", "role": "Escribir copy", "group": 1 },
{ "agent": "Director", "role": "Compilar y revisar", "group": 2 }
]
}
En este ejemplo, los 3 primeros pasos se ejecutan en paralelo real dentro de la misma request vía Promise.all. Cada agente recibe su propio prompt con contexto, capacidades y outputs anteriores. Cuando los 3 terminan (o fallan), el sistema encadena al grupo 2, donde el Director compila los resultados.
⚡ Éxito parcial
Si 2 de 3 agentes completan pero uno falla, la tarea continúa con los outputs disponibles. No se bloquea toda la orquestación por un fallo aislado — el Director recibe lo que hay y trabaja con ello.
🔄 Retry granular
Si un paso falla o hace timeout, se puede reintentar individualmente sin re-ejecutar toda la tarea. El sistema reutiliza la misma lógica de ejecución que el paso original, con acceso completo a los outputs previos como contexto.
Timeout Cascading: Capas de Seguridad
En cualquier sistema distribuido que ejecuta IA, las cosas van a fallar. Los LLMs se cuelgan, las APIs externas no responden, los requests de generación de imagen tardan más de lo esperado. El truco no es evitar fallos — es contenerlos elegantemente.
Diseñamos un sistema de timeouts en cascada donde cada capa es un poco más permisiva que la anterior. Si la capa interna no detecta el problema, la siguiente lo atrapa. Y si ninguna lo atrapa, el sistema de auto-recovery lo limpia periódicamente.
Timeout del proveedor de IA
Cada llamada al LLM/TTI/TTS tiene su propio timeout según la capacidad: generación de texto ~2 min, generación de imagen ~3 min, TTS ~3 min. Si el proveedor no responde, el paso falla limpiamente con un error descriptivo.
Hard timeout por paso
Cada paso de ejecución está envuelto en un Promise.race con un timeout duro. Si el paso completo — incluyendo pre-procesamiento, llamada IA, post-procesamiento y guardado — excede el límite, se cancela y se marca como timed_out. Esto es siempre mayor que el timeout del proveedor para darle margen al post-procesamiento.
Auto-recovery en servidor
Un proceso periódico revisa pasos que llevan demasiado tiempo en estado running. Si un paso lleva más de X minutos sin completar — probablemente porque su request fue terminado abruptamente — se marca como timed_out y se libera al agente. Este umbral es siempre mayor que el hard timeout.
Timeout en el frontend
El cliente tiene su propio temporizador que es el más permisivo de todos. Si tras todo el tiempo de espera no hay respuesta, muestra un estado de timeout al usuario con opción de reintentar. Cada capa es un poco más generosa que la anterior, formando un embudo de seguridad.
La regla de oro del cascading
Tprovider < Thard < Trecovery < Tfrontend
Si cualquiera de estas capas tiene un timeout menor que la anterior, se producen detecciones prematuras: el frontend marca timeout antes de que el servidor haya terminado, o el recovery limpia un paso que aún estaba procesando. El orden estricto garantiza que cada capa solo actúa si la anterior falló.
Una Sola Función, Tres Caminos
Cuando tienes ejecución paralela, secuencial y retry, es tentador escribir la lógica de cada camino por separado. Al fin y al cabo, tienen contextos ligeramente diferentes: el paralelo ejecuta múltiples agentes a la vez, el secuencial uno por uno, y el retry necesita reconstruir el contexto de un paso que ya existía.
Pero el núcleo es idéntico: cargar contexto de datos del agente, construir el prompt con outputs anteriores, llamar al LLM con el provider correcto, y post-procesar el resultado según la capacidad (texto, imagen, audio, visión). Si plantas esa lógica en 3 sitios, cada bug hay que arreglarlo 3 veces. Cada mejora hay que copiarla 3 veces. Cada inconsistencia es una bomba de relojería.
Ejecución paralela
Múltiples agentes en la misma request. Cada uno llama al core de ejecución con su propio contexto. Los resultados se agregan.
Ejecución secuencial
Un agente por request. Ejecuta el core, guarda resultado, y encadena al siguiente paso vía self-fetch.
Retry individual
Re-ejecuta un paso fallido con el mismo core. Reconstruye el contexto de outputs anteriores desde la base de datos.
La solución es extraer el core de ejecución en una función compartida. Esta función recibe el agente, el plan, los outputs previos y la configuración, y devuelve el resultado procesado. Los tres caminos la usan, pero cada uno maneja su propia lógica de estado, timeouts y encadenamiento alrededor de ella.
async function executeStep(agent, plan, prevOutputs, config) {
// 1. Contexto de datos (si el agente tiene acceso)
const dataContext = agent.dataAccess
? await loadOrganizationData(agent)
: '';
// 2. Búsqueda web (si el paso lo requiere)
const webContext = plan.capability === 'web'
? await searchWeb(task.title)
: '';
// 3. Construir prompt con outputs anteriores
const prompt = buildPrompt(agent, plan, prevOutputs, dataContext, webContext);
// 4. Llamar al provider correcto
const result = await callAI(config.provider, config.model, prompt);
// 5. Post-proceso según capacidad (TTI → generar imagen, TTS → sintetizar, etc.)
return await postProcess(result, plan.capability);
} Esta deduplicación no es cosmética. Cuando detectamos un bug en el manejo de web search, o mejoramos el post-procesamiento de imágenes, el fix se aplica a los tres caminos simultáneamente. Pasamos de ~360 líneas triplicadas a ~120 líneas compartidas.
La Base de Datos como Fuente de Verdad
En una arquitectura stateless como Workers, no puedes guardar estado en memoria entre requests. Cada invocación de /continue empieza desde cero: carga la tarea, lee los pasos completados, identifica cuál es el siguiente, y ejecuta. La base de datos es la única fuente de verdad.
| Estado del paso | Significado | Acción del sistema |
|---|---|---|
running | Ejecutándose ahora mismo | El paso está en ejecución. Si lleva demasiado, auto-recovery lo limpia. |
completed | Terminado exitosamente | Su output se incluye como contexto para los siguientes pasos. |
failed | Error en la ejecución | Se puede reintentar individualmente sin afectar el resto. |
timed_out | Excedió el timeout | El agente se libera. El paso puede reintentarse manualmente. |
Esta granularidad por paso — no por tarea — es lo que hace posible el retry selectivo. El usuario puede ver en tiempo real qué pasos completaron, cuáles fallaron, y reintentar solo los problemáticos. El sistema reconstruye el contexto a partir de los outputs existentes y re-ejecuta exclusivamente lo necesario.
Feedback Progresivo en Tiempo Real
El patrón self-fetch chain abre la puerta a una UX que sería imposible con ejecución monolítica. Si toda la orquestación ocurriera en un solo request, el usuario vería un spinner durante 3 minutos sin saber qué está pasando. Con la cadena, el frontend puede pollear el estado y mostrar progreso real.
📊 Progreso por pasos
El frontend muestra cada paso con su agente, estado y resultado. Los pasos paralelos se identifican visualmente con un badge. El usuario sabe exactamente qué agente está trabajando y cuántos pasos faltan.
⏱️ Timeout visual
El frontend tiene su propio temporizador. Si un paso lleva más de lo esperado, se muestra un indicador de "posible timeout" antes de que el servidor lo confirme. El usuario puede decidir esperar o reintentar.
🔄 Auto-recovery transparente
Si el sistema detecta un paso atascado y lo recupera automáticamente, se registra como evento visible. El usuario ve "Auto-recovered: stuck for Xs" en el timeline de la tarea.
🤝 Eventos de colaboración
Cada paso completado emite un evento de colaboración que aparece en el feed de actividad. Los usuarios ven quién contribuyó a qué, sin importar si la ejecución fue paralela o secuencial.
Lo que Aprendimos Construyendo Esto
1. Los timeouts se diseñan de fuera hacia dentro, pero se implementan de dentro hacia fuera
Primero defines cuánto está dispuesto a esperar el usuario (frontera exterior). Luego trabajas hacia dentro: el servidor necesita menos que el frontend, el hard timeout menos que el recovery, y el provider menos que el hard timeout. Es la misma idea que las capas de un sistema operativo.
2. No duplicar lógica de ejecución — sin excepciones
Tuvimos la lógica de ejecución triplicada durante un tiempo. Cada bug se arreglaba en un sitio y reaparecía en otro. La primera inversión de deuda técnica fue extraer el core compartido. La regla ahora: si algo se ejecuta igual en dos caminos, se abstrae.
3. La base de datos como máquina de estados es sorprendentemente robusta
En lugar de mantener estado en memoria o en un queue externo, cada request lee el estado actual y decide qué hacer. Esto hace el sistema naturalmente idempotente — si un /continue se ejecuta dos veces, el segundo no encuentra pasos pendientes y termina sin hacer nada.
4. El éxito parcial es más útil que el fallo total
Si 2 de 3 agentes paralelos completan, el resultado parcial suele ser valioso. El Director puede compilar con lo que tiene. Es mejor dar un resultado al 80% que fallar completamente y obligar al usuario a re-ejecutar todo desde cero.
5. El self-fetch es invisible para el resto del sistema
El frontend, las APIs de estado, el cron de auto-recovery — ninguno sabe que la ejecución usa self-fetch chain internamente. Solo ven pasos que cambian de estado en la base de datos. Esa transparencia arquitectónica es la señal de que el patrón está bien encapsulado.
Resumen técnico
- ✦ Self-Fetch Chain: cada paso en un request independiente, encadenados vía
waitUntil(fetch) - ✦ Grupos paralelos: pasos con mismo
parallel_groupse ejecutan enPromise.all - ✦ Timeout cascading: 4 capas (provider → hard → recovery → frontend) con orden estricto
- ✦ Core de ejecución compartido: una función, tres caminos (paralelo, secuencial, retry)
- ✦ Estado en base de datos: máquina de estados naturalmente idempotente
- ✦ Éxito parcial: la orquestación continúa con los agentes que completaron
- ✦ Retry granular: re-ejecutar pasos individuales sin repetir toda la tarea
- ✦ Auto-recovery: limpieza automática de pasos atascados con umbrales configurables
- ✦ 100% edge-native: sin servidores persistentes, sin queues externos, sin WebSockets
Cadences Engineering
Documentación técnica del equipo de ingeniería
Synapse: El Cerebro de Datos
El sistema nervioso central
Artículo relacionado →Arquitectura Multi-Tenant
Cómo Cadences escala para miles de orgs