wandres.dev
CACHE API · la cache del edge

La Cache API: `caches.default` y caches con nombre

El edge ya cachea sin que se lo pidas; la Cache API te da la llave de ese almacén. Qué es `caches.default`, en qué se diferencia de una cache abierta con `caches.open`, por qué lo que guardas vive en un solo centro de datos y qué respuestas acepta o rechaza el runtime cuando intentas cachear una `Response` a mano.

⏱ 17 min

Todo lo que has construido hasta aquí —KV, R2, D1, Durable Objects, Hyperdrive— resuelve el problema de dónde vive el dato. La Cache API resuelve uno distinto y más antiguo: cómo no volver a calcular lo que ya calculaste. Cloudflare lleva décadas cacheando en el edge de forma automática, guiado por extensiones de archivo y cabeceras, y ese comportamiento por defecto sigue ahí aunque nunca escribas una línea. Lo que la Cache API añade es control: te entrega ese mismo almacén como un objeto de JavaScript sobre el que puedes mirar, escribir y borrar dentro de tu Worker, con la granularidad que tú decidas. Es la diferencia entre confiar en una heurística y programar una política. Y como toda llave que abre una puerta grande, exige entender qué hay al otro lado: un almacén rapidísimo, volátil y —esto sorprende a casi todo el mundo— estrictamente local.

🎯 Al terminar esta lección sabrás
  • Situar la Cache API dentro del pipeline de cache de Cloudflare y distinguirla del cacheo automático del edge.
  • Usar caches.default para leer y escribir en la misma cache que emplea la CDN de tu zona.
  • Abrir espacios aislados con caches.open y entender qué ganas y qué pierdes al hacerlo.
  • Reconocer qué respuestas acepta put, cuáles rechaza y por qué el almacén es por centro de datos.

El almacén que ya estaba ahí

Antes de que exista tu Worker, Cloudflare ya tiene una cache en cada uno de sus centros de datos. Cuando una petición llega a la red, el pipeline decide —según la extensión del recurso, las cabeceras de la respuesta del origen y las reglas de cache configuradas en la zona— si esa respuesta se guarda y durante cuánto. Ese mecanismo es implícito y opaco: funciona bien para assets estáticos y se queda corto en cuanto quieres cachear el resultado de una consulta a D1, la respuesta de una API de terceros o un fragmento de HTML generado.

La Cache API expone ese almacén con la interfaz estándar que definió el estándar de Service Workers: un objeto global caches, del que obtienes una cache concreta y sobre el que llamas match, put y delete. Que sea la misma interfaz del navegador no es cosmética: significa que el modelo mental que ya tienes —una cache es un mapa de peticiones a respuestas— se transfiere íntegro, y que el código que escribes se lee igual en el cliente y en el edge.

La diferencia de fondo con el cacheo automático es de naturaleza, no de grado. El pipeline decide con información limitada —lo que puede deducir de una URL y unas cabeceras— y aplica la misma política a todo el que pasa. Tu Worker decide con toda la información disponible: quién pregunta, desde dónde, con qué parámetros, en qué estado está tu sistema. Puedes cachear una respuesta durante diez segundos si el catálogo está en promoción y durante una hora si no, o guardar el resultado de un cálculo que jamás viajó por HTTP y que ninguna heurística podría haber identificado como cacheable.

export default {
  async fetch(request, env, ctx): Promise<Response> {
    const cache = caches.default;
    const cacheada = await cache.match(request);
    if (cacheada) return cacheada;

    const respuesta = new Response(JSON.stringify({ hora: Date.now() }), {
      headers: {
        "content-type": "application/json",
        "cache-control": "public, s-maxage=60",
      },
    });

    ctx.waitUntil(cache.put(request, respuesta.clone()));
    return respuesta;
  },
} satisfies ExportedHandler<Env>;

Fíjate en dos detalles que la lección siguiente convierte en doctrina: el clone antes de guardar —un cuerpo de respuesta solo puede leerse una vez, y put lo consume— y el ctx.waitUntil, que deja la escritura corriendo en segundo plano sin retrasar ni un milisegundo lo que ve el usuario.

La implementación del edge no es el estándar completo, y conviene saber qué falta antes de echarlo de menos. Tienes match, put y delete, que es el triángulo con el que se construye todo. No tienes add ni addAll —los atajos que descargan y guardan de una sola vez—, ni keys para enumerar lo almacenado, ni matchAll. Esa ausencia de enumeración no es un olvido: recorrer el contenido de una cache distribuida en cientos de emplazamientos no sería una operación con un significado claro, y ofrecerla invitaría a razonar sobre el almacén como si fuera una base de datos. La Cache API no es un índice que puedas inspeccionar; es un oráculo al que preguntas por una clave concreta y que responde con un recurso o con nada.

const encontrada = await cache.match(clave);
if (encontrada === undefined) {
  // no hay copia en este centro de datos: hay que trabajar
}

Ese valor ausente merece atención porque es la respuesta más frecuente en un sistema recién desplegado y la que define el comportamiento de tu código. Un fallo de cache no es un error ni una condición excepcional: es el estado normal de cualquier entrada la primera vez que alguien la pide en cada uno de los emplazamientos de la red.

caches.default frente a las caches con nombre

Hay dos formas de conseguir una cache y la elección no es de estilo. caches.default te devuelve la cache de la zona: exactamente la misma que usa la CDN cuando sirve tus assets. Escribir ahí significa entrar en el flujo normal de Cloudflare, con todo lo que eso implica —tus entradas conviven con las que puso el pipeline, responden a las mismas reglas de cache y, sobre todo, son alcanzables por el purgado desde el panel o la API—.

caches.open abre un espacio con nombre propio, un compartimento estanco. Lo que guardas ahí es tuyo y de nadie más: no lo pisa el pipeline, no lo devuelve caches.default, y te permite tener varias generaciones o varios dominios de datos separados sin colisiones de clave. El precio es la orfandad operativa: una cache con nombre no aparece en las herramientas de purgado de la zona, así que solo puedes vaciarla expirándola por tiempo o borrando entrada por entrada desde un Worker.

const fragmentos = await caches.open("fragmentos:v3");
await fragmentos.put(clave, respuesta.clone());

El sufijo de versión en ese nombre no es decorativo, y la última lección del nivel lo desarrollará hasta convertirlo en una estrategia completa. Como una cache con nombre no se puede purgar desde fuera, el único mecanismo de invalidación masiva que te queda es dejar de usar un nombre y empezar a usar otro: las entradas huérfanas del espacio anterior expiran solas y nadie vuelve a consultarlas. Nombrar la cache con una versión es, de hecho, construir de antemano la salida de emergencia.

📝
Ningún espacio es infinito ni permanente

La cache del edge desaloja entradas cuando necesita sitio, con criterios que no controlas y que dependen del uso de cada emplazamiento. Nada de lo que escribas está garantizado en la siguiente petición, aunque su TTL no haya vencido. Esa es la diferencia esencial con KV o R2 y la regla que debe gobernar tu código: la cache es un acelerador, jamás la única copia de un dato.

🌍

caches.default

La cache de la zona. Interopera con la CDN y con el purgado por URL o por etiqueta. Tu elección por defecto para respuestas HTTP normales.

📦

caches.open

Un espacio aislado por nombre. Ideal para datos intermedios o versionados; nadie más lo toca, pero tampoco nadie más lo purga.

📍

Ámbito local

Ambas viven en el centro de datos donde corrió el Worker. Ni son globales ni se replican: un acierto en Madrid no implica un acierto en Tokio.

Qué acepta put y qué rechaza

put no es un set de diccionario: es una operación HTTP con reglas. La petición que actúa de clave debe ser un GET; cualquier otro método provoca un error. La respuesta no puede ser un 206 de contenido parcial ni llevar Vary: *, porque en ambos casos la entrada sería ambigua. Si la respuesta trae Set-Cookie, el runtime se niega a guardarla —protegiéndote de servir la sesión de un usuario a otro— salvo que declares explícitamente Cache-Control: private=Set-Cookie para que esa cabecera se elimine al cachear.

El tiempo de vida tampoco lo eliges con un parámetro: sale de la propia respuesta. s-maxage manda sobre max-age para las caches compartidas, Expires sirve de alternativa antigua, y no-store impide el guardado por completo. La consecuencia práctica es que controlas la cache construyendo cabeceras, no pasando opciones: si quieres que algo dure un minuto en el edge, lo dices en la Response antes de entregarla a put.

Cuando el recurso viene de un origen que no controlas, esa regla obliga a reescribir la respuesta antes de guardarla. El patrón es siempre el mismo: se construye una respuesta nueva sobre el mismo cuerpo, con las cabeceras corregidas.

const delOrigen = await fetch(peticionAlOrigen);

const paraCachear = new Response(delOrigen.body, delOrigen);
paraCachear.headers.set("cache-control", "public, s-maxage=600");
paraCachear.headers.delete("set-cookie");

ctx.waitUntil(cache.put(clave, paraCachear.clone()));

Borrar set-cookie a mano antes de cachear parece agresivo y lo es: estás decidiendo que esa cabecera no debe llegar a una copia compartida. Hazlo solo cuando sepas con certeza que la cookie no transporta identidad; si transporta sesión, la respuesta entera no es candidata a cache compartida y ninguna limpieza de cabeceras arregla eso.

⚠️
En `workers.dev` la Cache API no hace nada

Los dominios workers.dev no pasan por la cache de zona, así que las llamadas a put se aceptan sin error y match devuelve siempre vacío. Es la trampa número uno de este nivel: pruebas tu cache-aside en previsualización, ves cero aciertos, y concluyes que tu código está mal cuando lo único que falta es un dominio propio. Prueba siempre sobre una ruta de tu zona.

flowchart TD
P[Peticion llega al centro de datos] --> W[El Worker se ejecuta]
W --> M[cache.match sobre caches.default]
M -->|acierto| H[Respuesta desde el almacen local]
M -->|fallo| T[Trabajo real: origen o computo]
T --> V[Validar cabeceras y metodo]
V -->|apto| G[cache.put guarda la copia]
V -->|no apto| S[Se sirve sin guardar]
style H fill:#a6e3a1,color:#11111b
style S fill:#f38ba8,color:#11111b

Ese diagrama esconde la propiedad más importante del sistema: el nodo de entrada es un centro de datos concreto. La Cache API no es un almacén distribuido con una vista consistente como KV; es un conjunto de cientos de caches independientes, cada una con su propio contenido, sus propios aciertos y sus propias expiraciones. Un usuario en Lima y otro en Fráncfort pueden ver estados distintos de la misma clave sin que eso sea un error.

Esa localidad tiene una lectura optimista que conviene no perder de vista. Precisamente porque no hay coordinación entre nodos, la escritura es baratísima y la lectura es lo más rápido que ofrece la plataforma: los bytes ya están en el mismo edificio donde corre tu isolate, sin viaje de red, sin consenso, sin réplica. La Cache API es, en latencia, el almacenamiento más veloz al que puede acceder un Worker, y su precio es exactamente la garantía que ha renunciado a darte. Elegirla frente a KV no es una cuestión de gusto sino de qué prometes: KV te da una vista global eventualmente consistente que puedes leer desde cualquier parte; la cache te da velocidad absoluta a cambio de no prometer nada sobre lo que verá el siguiente.

ℹ️
Cuándo esto es exactamente lo que quieres

El perfil ideal es un recurso caro de producir, idéntico para todos los que lo piden y solicitado muchas veces desde la misma región: el resultado de una agregación sobre D1, un fragmento de HTML renderizado, la respuesta de una API externa con límite de peticiones. En todos esos casos la localidad no molesta —el tráfico ya está concentrado donde está la audiencia— y el ahorro es inmediato desde la segunda petición.

Cachear es materializar una función en el tiempo

Conviene resistir la tentación de ver la cache como un simple almacén rápido, porque esa metáfora se agota enseguida y no explica nada de lo que de verdad ocurre. Una cache es una afirmación matemática sobre tu código: dices que cierta parte de tu sistema se comporta como una función pura durante una ventana de tiempo, y que por tanto puedes sustituir su ejecución por su resultado. Todo lo demás —las cabeceras, las claves, los tiempos de vida— es la maquinaria que hace cumplir esa afirmación. El s-maxage no mide segundos: mide durante cuánto tiempo estás dispuesto a apostar que la respuesta de antes sigue siendo la respuesta de ahora. Cuando lo ves así, los errores de cache dejan de parecer accidentes técnicos y se revelan como lo que son, errores de razonamiento: cachear una respuesta personalizada es afirmar que una función que depende del usuario no depende del usuario; cachear con un tiempo largo algo que cambia rápido es apostar a un plazo que no puedes sostener. Y aquí aparece el segundo salto, el que separa al que usa cachés del que las diseña. En un servidor tradicional, la cache es una capa que añades delante de tu lógica. En el edge, la cache es el sustrato sobre el que la lógica ocurre: tu Worker se ejecuta dentro del propio nodo de cache, y por eso puedes decidir petición a petición qué se materializa y qué se recalcula, con toda la información del contexto en la mano. Esa inversión te da un poder que la arquitectura clásica no tiene y una responsabilidad idéntica: ya no puedes echarle la culpa a la CDN de lo que cachea, porque la CDN eres tú. La pregunta de ingeniería deja de ser «¿qué pongo detrás de la cache?» y pasa a ser «¿qué partes de mi sistema puedo prometer estables, durante cuánto, y para quién?». Contestarla bien es, casi literalmente, diseñar el rendimiento del producto.

⚔️ Abre el almacén con tus propias manos
  1. Escribe un Worker desplegado en una ruta de tu zona que devuelva la marca de tiempo actual con Cache-Control: public, s-maxage=30, guardándola en caches.default, y observa cómo la respuesta se congela durante medio minuto.
  2. Repite el experimento en un dominio workers.dev y explica con precisión por qué la marca de tiempo cambia siempre.
  3. Duplica el Worker usando caches.open con dos nombres distintos y comprueba que un match en uno nunca ve lo escrito en el otro.
  4. Intenta cachear una respuesta que incluya Set-Cookie y otra con método POST; documenta exactamente qué ocurre en cada caso y por qué el runtime se comporta así.
  5. Pide la misma URL desde dos redes geográficamente lejanas y razona por qué la primera petición de cada una paga el coste completo.