Blog
Arquitectura · 14 min lectura · 20 marzo 2026

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.

El problema

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.

El patrón

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

POST /run → Orquesta plan → Ejecuta paso 1 (CEO) → Guarda resultado
self-fetch via waitUntil
POST /continue → Detecta grupo paralelo → Ejecuta pasos 2 y 3 en Promise.all
self-fetch via waitUntil
POST /continue → Ejecuta paso 4 (compilación) → Finaliza tarea → Genera summary
💡

¿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.

Ejecución paralela

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 de orquestación generado por IA
{
  "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.

Resiliencia

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.

L1

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.

L2

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.

L3

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.

L4

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ó.

Ingeniería

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.

// Pseudocódigo: core de ejecución compartido
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.

Estado

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.

Experiencia de usuario

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.

Aprendizajes

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_group se ejecutan en Promise.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
C

Cadences Engineering

Documentación técnica del equipo de ingeniería