wandres.dev
REQUEST Y RESPONSE · streaming en el edge

Subrequests con fetch: proxy, agregación de APIs y caché del edge

Dentro de un Worker, fetch deja de ser el cliente del navegador y pasa a ser una llamada saliente hacia cualquier origen: un subrequest. Cada uno cuenta contra un presupuesto que depende del plan, y solo seis pueden viajar a la vez. Sobre esa primitiva se levantan los dos patrones que definen al Worker como intermediario —el proxy inverso y la agregación de varias APIs— y la caché del edge que abarata repetirlos.

⏱ 17 min

El mismo fetch que usarías en el navegador vive también dentro del Worker, pero con el sentido invertido: ya no pide recursos para pintar una página, sino que sale desde el edge hacia cualquier origen de Internet o servicio de Cloudflare para traerte datos que compondrás en tu respuesta. Cada una de esas llamadas salientes es un subrequest, y ahí empieza el oficio: los subrequests se cuentan, tienen un techo por invocación y solo seis pueden viajar en paralelo. Sobre esa primitiva se construyen los dos patrones que convierten a tu Worker en un intermediario de verdad —el proxy inverso y la agregación de varias APIs— y la caché que hace que repetirlos apenas cueste.

🎯 Al terminar esta lección sabrás
  • Entender que fetch dentro del Worker lanza peticiones salientes llamadas subrequests.
  • Conocer el presupuesto por invocación —50 o 1000 según el plan— y el techo de seis conexiones simultáneas.
  • Construir un proxy inverso que reenvía una petición a un origen y retoca su respuesta.
  • Agregar varias APIs en paralelo y cachear subrequests con el objeto cf.

El fetch que sale del Worker

Cuando llamas a fetch dentro de un handler, usas la misma API estándar de siempre, pero el Worker actúa ahora como cliente: abre una conexión desde el edge de Cloudflare hacia el origen que le indiques y espera su Response. Esa llamada saliente es un subrequest, y da igual que apunte a una API pública de Internet o a un binding de R2, KV o D1: todas nacen del mismo gesto y todas se contabilizan.

El caso más simple es traer datos de una API y reexponerlos. Fíjate en que la Response que recibes de fetch es de solo lectura en sus cabeceras: si quieres tocarlas, construye una nueva a partir de ella en lugar de mutarla.

export default {
  async fetch(request: Request): Promise<Response> {
    // fetch saliente: el Worker es el cliente de otro servidor
    const origen = await fetch("https://api.example.com/datos");
    const datos = await origen.json();
    return Response.json(datos);
  },
};

Nada distingue en la firma a este fetch del que usa un navegador; lo que cambia es el contexto de ejecución. Corre en el centro de datos que atendió la petición, así que la latencia hacia el origen se mide desde el edge y no desde el dispositivo del usuario, y por eso agregar varias APIs cerca del origen suele salir más barato que hacerlo desde el cliente.

Ese gesto se generaliza enseguida. Si en lugar de fabricar una URL fija quieres reenviar la propia petición entrante, pásale el Request original a fetch: copiará método, cabeceras y cuerpo. Y si necesitas cambiar algo por el camino —la ruta, una cabecera de autorización— construyes un Request nuevo a partir del original antes de enviarlo, porque el que recibes también es inmutable.

📝
Cuidado con el bucle sobre tu propia ruta

Si el destino de tu fetch vuelve a caer sobre una ruta que sirve este mismo Worker, cada invocación llamará a la siguiente y montarás un bucle que agota el presupuesto de subrequests en una sola petición. Cuando proxias, apunta a un origen externo o a otro Worker vía service binding, nunca a tu propia ruta pública sin una condición de corte.

El presupuesto: subrequests y conexiones

Cada fetch saliente gasta de un presupuesto acotado, y el número exacto depende del plan. Además, cada salto de una cadena de redirecciones cuenta como un subrequest más, así que el total real puede superar a las llamadas que ves escritas.

🆓

Plan Free

50 subrequests externos por invocación, más 1000 hacia servicios internos de Cloudflare como KV, R2 o D1.

💳

Plan Paid

El tope clásico eran 1000 por invocación; desde 2026 el valor por defecto es 10000 y sube con limits.subrequests.

🔌

Seis conexiones

Da igual el plan: solo seis conexiones salientes pueden viajar en paralelo en un mismo instante.

La cifra que de verdad moldea tu diseño no es el techo total, sino ese límite de seis conexiones simultáneas. Puedes hacer miles de subrequests a lo largo de una petición, pero nunca más de seis viajando a la vez. Por eso un abanico ingenuo de cien llamadas en paralelo no acelera nada: se encola contra esa barrera y conviene agruparlo en oleadas. El corolario práctico es que la latencia de una agregación la fija la oleada más lenta, no la suma de todas: seis llamadas de cien milisegundos lanzadas juntas tardan cien, no seiscientos.

⚠️
Las redirecciones se pagan

Un fetch a una URL que responde con tres redirecciones encadenadas consume cuatro subrequests, no uno. Si proxias orígenes que redirigen mucho, vigila el gasto: puedes fijar redirect: "manual" en el fetch para inspeccionar el Location tú mismo y decidir si sigues el salto o lo cortas.

Proxy inverso y agregación de APIs

Dos arquetipos cubren casi todo. El proxy inverso reenvía la petición entrante a un origen, quizá retocando la ruta o las cabeceras, y devuelve su respuesta como si el Worker fuera el servidor. La agregación dispara varios subrequests, espera a todos y combina sus resultados en una sola respuesta, ahorrándole al cliente varias rondas de red.

Un proxy inverso mínimo compone la URL del origen a partir de la ruta entrante y reenvía la petición tal cual; a la vuelta, envuelve la respuesta para anotar una cabecera propia sin mutar la original.

// proxy inverso: reescribe la ruta y reenvia al origen
export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const destino = `https://origen.example.com${url.pathname}`;
    const arriba = await fetch(destino, request);
    const respuesta = new Response(arriba.body, arriba);
    respuesta.headers.set("X-Servido-Por", "mi-worker");
    return respuesta;
  },
};

La agregación cambia la forma: en vez de un origen, varios, lanzados a la vez y fundidos en una sola carga útil.

export default {
  async fetch(request: Request): Promise<Response> {
    // agregar tres APIs en una sola respuesta
    const [perfil, pedidos, pagos] = await Promise.all([
      fetch("https://api.example.com/perfil").then((r) => r.json()),
      fetch("https://api.example.com/pedidos").then((r) => r.json()),
      fetch("https://api.example.com/pagos").then((r) => r.json()),
    ]);
    return Response.json({ perfil, pedidos, pagos });
  },
};

El Promise.all lanza las tres a la vez, y como son tres caben holgadas bajo el techo de seis conexiones. Si fueran cien, tendrías que trocear el abanico en lotes; ese detalle lo profundizas en la lección de memoria y subrequests del nivel de límites. La diferencia con el proxy es de intención: el proxy preserva y reenvía una respuesta ajena, mientras que la agregación fabrica una respuesta nueva a partir de varias.

Ambos patrones se combinan a menudo: un mismo Worker proxia el HTML de un origen y, en paralelo, agrega los datos de dos APIs para inyectarlos en esa página. El presupuesto de subrequests y el techo de conexiones son el marco dentro del cual esa orquestación se mantiene sana.

flowchart LR
C[Cliente] --> W[Worker]
W --> A1[API perfil]
W --> A2[API pedidos]
W --> A3[API pagos]
A1 --> J[Combinar en un JSON]
A2 --> J
A3 --> J
J --> R[Respuesta unica]

Cachear subrequests con cf

Cada fetch saliente admite un objeto cf en su init que controla cómo lo trata la caché de Cloudflare. Por defecto, un GET respeta las cabeceras de caché del origen; con cacheTtl fuerzas un tiempo de vida, con cacheEverything guardas incluso respuestas que el origen no marcó como cacheables, y con cacheKey normalizas la clave para que peticiones distintas compartan la misma entrada.

const res = await fetch("https://api.example.com/catalogo", {
  cf: {
    cacheTtl: 300,          // cachea 300 segundos en el edge
    cacheEverything: true,  // aun si el origen no lo marco cacheable
  },
});

Conviene entender el comportamiento por defecto antes de forzar nada: sin objeto cf, Cloudflare ya cachea las respuestas GET que el origen marca como cacheables y respeta las que declara privadas o los POST. El objeto cf no enciende la caché, sino que la reajusta cuando el criterio del origen no te sirve.

Cachear un subrequest lo convierte, tras la primera vez, en un acierto de caché que no vuelve a salir a la red ni gasta una conexión: el segundo visitante en ese centro de datos recibe la respuesta guardada. Es la palanca más barata para que un proxy o una agregación escalen sin machacar al origen.

💡
Una clave de caché estable multiplica los aciertos

Si tu URL lleva parámetros que no cambian el contenido —un utm_source, un token de sesión— cada variante genera una entrada distinta y el ratio de aciertos se desploma. Fija un cacheKey que ignore esos parámetros y de golpe muchas peticiones que parecían únicas comparten una sola entrada cacheada.

El subrequest convierte tu Worker en un nodo, no en una hoja

Hasta esta lección tu Worker era el final del recorrido: llegaba una petición y él, con lo que tenía a mano, fabricaba la respuesta. El subrequest lo cambia de categoría. En cuanto tu código puede llamar hacia afuera, deja de ser una hoja del árbol y pasa a ser un nodo intermedio: un punto de la red que recibe, consulta a otros, compone y reenvía. Y esa promoción no es un detalle técnico, es un cambio de identidad, porque casi toda la arquitectura interesante del edge vive en ese papel de intermediario. El proxy inverso y la agregación no son dos trucos sueltos: son las dos formas fundamentales de mediar, ocultar un origen tras una fachada que controlas, o fundir varias voces en una sola para que el cliente no pague la latencia de hablar con cada una. Pero un intermediario sin disciplina colapsa la malla que habita, y por eso el modelo te impone un presupuesto y, sobre todo, ese techo de seis conexiones que parece mezquino hasta que entiendes que te protege a ti y a los sistemas que consultas de un abanico descontrolado. La caché cierra el círculo con la elegancia justa: transforma el subrequest repetido en un gesto casi gratuito, de modo que mediar deja de ser caro y se vuelve el modo natural de trabajar. Cuando dejas de ver fetch como “traer un dato” y empiezas a verlo como “componer servicios”, has dado el salto de programar endpoints a diseñar topologías, y ese es el oficio real en un mundo donde tu código se sienta en medio de todo.

⚔️ Compón servicios desde el edge
  1. Escribe un proxy inverso que reenvíe la petición entrante a un origen y le añada una cabecera X-Servido-Por a la respuesta construyendo una Response nueva.
  2. Agrega tres APIs con Promise.all en un único JSON, y mide cuánto tarda frente a llamarlas en serie con tres await seguidos.
  3. Provoca un gasto oculto de subrequests apuntando a una URL que redirige, y arréglalo inspeccionando el Location con redirect: "manual".
  4. Cachea uno de los subrequests con cf.cacheTtl y comprueba, con dos peticiones seguidas, que la segunda no vuelve a salir al origen.
  5. Explica por qué disparar cien fetch en paralelo con Promise.all no es cien veces más rápido, y reescríbelo en lotes de seis.