Cloudflare CDN y caché a fondo: qué se cachea, Cache Rules, Tiered Cache, purgas y Cache API

Cómo decide Cloudflare qué cachear y durante cuánto, qué significa cada cf-cache-status, Cache Rules, Tiered Cache, Cache Reserve, purgas y la Cache API de Workers, con límites por plan, pruebas reales con curl y un Worker que cachea respuestas probado en local.

Portada: Cloudflare CDN y caché a fondo

La caché es la razón original de Cloudflare: guardar copias de tus ficheros en cientos de centros de datos para que el visitante las reciba del más cercano y tu servidor no tenga que servirlas una y otra vez. Funciona sola desde el primer día, pero con unas reglas por defecto que sorprenden a mucha gente. La más importante: Cloudflare no cachea tu HTML ni tu JSON salvo que se lo pidas.

En este artículo vemos cómo decide Cloudflare qué cachear y durante cuánto tiempo, cómo leer la cabecera cf-cache-status, las Cache Rules, el Tiered Cache, Cache Reserve, las purgas y la Cache API de Workers, con lo que incluye cada plan. Y lo comprobamos con curl contra este mismo blog y con un Worker que cachea respuestas en local.

🧪
Las pruebas con curl se han hecho de verdad el 3 de octubre de 2026 contra este blog, que está detrás de Cloudflare. El Worker con la Cache API se ha ejecutado en local con Wrangler 4.147.0 y Node.js 24.21.0. Cache Rules, Tiered Cache, Cache Reserve y las purgas no los he probado: necesitan acceso a la configuración de una zona y no he tocado la del blog. Límites y precios comprobados en la documentación oficial ese mismo día.

Cómo funciona

Cuando una petición llega a un centro de datos de Cloudflare para un registro con proxy (nube naranja), Cloudflare decide primero si la respuesta puede cachearse. Si puede, la busca en su caché local. Si está, la sirve (HIT). Si no, la pide al origen, la sirve y la guarda (MISS). Con Tiered Cache, antes de ir al origen pregunta a un centro de datos «superior» que agrupa las peticiones de muchos otros.

Esquema de la caché de Cloudflare: el visitante pide un fichero al centro de datos cercano; si está en caché se sirve; si no, se consulta un centro superior (Tiered Cache) y solo este llega al origen; Cache Reserve guarda copias persistentes en R2
Con Tiered Cache, solo los centros superiores hablan con tu origen. Diagrama: devutilites.

Qué se cachea por defecto

Cloudflare decide por extensión del fichero, no por tipo MIME. Por defecto cachea imágenes, CSS, JS, fuentes, PDF, vídeo, audio, comprimidos y unos cuantos formatos más (también el robots.txt). No cachea HTML ni JSON. Para cachear algo más, necesitas una Cache Rule.

Aunque la extensión sea cacheable, Cloudflare no guarda la respuesta si:

  • el origen envía Cache-Control con private, no-store, no-cache o max-age=0;
  • la respuesta trae Set-Cookie;
  • el método no es GET.

Y sí la guarda si Cache-Control es public con max-age mayor que 0, o si Expires es una fecha futura. Si hay max-age y Expires, gana max-age. Si el origen no manda ninguna de las dos, Cloudflare aplica un TTL por código de estado:

CódigoTTL por defecto
200, 206, 301120 min
302, 30320 min
404, 4103 min
RestoNo se cachea

Para el CDN, s-maxage tiene prioridad sobre max-age: es la directiva pensada para cachés compartidas. Así puedes decir «el navegador, 30 segundos; Cloudflare, una hora».

Un detalle útil: si llegan muchas peticiones a la vez al mismo centro de datos para un fichero que no está en caché, Cloudflare solo manda una al origen y las demás esperan a esa respuesta (request collapsing).

Leer cf-cache-status

ValorSignificado
HITServido desde la caché
MISSCacheable, pero no estaba; se pidió al origen (y se guarda)
EXPIREDEstaba, pero caducado; se pidió de nuevo al origen
STALECaducado, servido de caché porque el origen no respondía
UPDATINGCaducado, servido de caché mientras se actualiza en segundo plano
REVALIDATEDEl origen confirmó con una petición condicional que no había cambiado
BYPASSEra cacheable, pero algo (cabeceras, una regla) hizo que no se cachease
DYNAMICNo es cacheable; se fue al origen sin mirar la caché

Comprobado con curl

Este mismo blog está detrás de Cloudflare. La portada (HTML) y una hoja de estilos, pedida dos veces, y después con otra query string:

$ curl -sI https://blog.devutilites.com/ | grep -iE "^HTTP|cf-cache-status|cache-control"
HTTP/2 200 
cache-control: public, max-age=0
cf-cache-status: DYNAMIC
$ curl -sI "https://blog.devutilites.com/public/cards.min.css?v=WwnU9jw5ancNC8Gc" | grep -iE "^HTTP|cf-cache-status|cache-control|^age"
HTTP/2 200 
cache-control: public, max-age=31536000
cf-cache-status: MISS
$ curl -sI "https://blog.devutilites.com/public/cards.min.css?v=WwnU9jw5ancNC8Gc" | grep -iE "^HTTP|cf-cache-status|cache-control|^age"
HTTP/2 200 
cache-control: public, max-age=31536000
age: 0
cf-cache-status: HIT
$ curl -sI "https://blog.devutilites.com/public/cards.min.css?v=prueba-21511" | grep -iE "^HTTP|cf-cache-status"
HTTP/2 200 
cf-cache-status: MISS

Se ve todo lo anterior: el HTML sale DYNAMIC (no se cachea por defecto), el CSS sale MISS la primera vez y HIT la segunda, y basta cambiar la query string para volver a MISS, porque la query string forma parte de la clave de caché. Es lo que aprovechan los temas que añaden ?v=… a sus ficheros para forzar que se descarguen de nuevo tras un cambio.

Cache Rules

Las Cache Rules usan el mismo lenguaje de reglas que el WAF: una expresión y lo que quieres hacer con las peticiones que la cumplen. Con ellas puedes:

  • marcar como cacheable lo que no lo es por defecto (HTML, JSON de una API pública…) o al revés, excluir rutas;
  • fijar el Edge TTL (cuánto guarda Cloudflare) y el Browser TTL (cuánto guarda el navegador), ignorando o respetando al origen;
  • personalizar la clave de caché (ignorar la query string, incluir una cabecera o una cookie).
Cache RulesFreeProBusinessEnterprise
Número de reglas102550300

Requieren registros con proxy. Y una advertencia de la documentación: si la clave de caché incluye cabeceras, cookies u otras propiedades de la petición, la purga de un solo fichero desde el panel deja de funcionar, porque el panel no puede enviar esos valores.

🚨
Cachear HTML de páginas con sesión es peligroso. Si una regla cachea /cuenta y la primera visita es de un usuario logueado, los siguientes visitantes pueden ver su página. Excluye siempre las rutas con contenido personal o usa una cookie de sesión para saltarte la caché.

Tiered Cache

Sin Tiered Cache, cada centro de datos que no tiene un fichero se lo pide a tu origen. Con Tiered Cache, los centros se dividen en inferiores y superiores: los inferiores preguntan primero a un superior y solo los superiores hablan con tu origen. El resultado es más aciertos de caché y muchas menos conexiones a tu servidor.

Tiered CacheFreeProBusinessEnterprise
Tiered Cache con topología SmartSíSíSíSí
Generic Global, Regional, CustomNoNoNoSí

La topología Smart elige automáticamente el mejor centro superior para tu origen según la latencia medida. Es gratis y conviene activarla. La documentación avisa de que, si cambias la IP de tu origen, puede cambiar el centro superior asignado y verás más MISS durante un tiempo.

Cache Reserve

La caché normal es efímera: los ficheros poco pedidos se expulsan. Cache Reserve añade una capa persistente montada sobre R2 para que tus ficheros cacheables sigan ahí aunque se pidan poco. Requiere un plan de pago de Cache Reserve y se factura por uso:

ConceptoPrecio
Almacenamiento0,015 $ por GB y mes
Operaciones de clase A (escrituras, en cada fallo de caché)4,50 $ por millón
Operaciones de clase B (lecturas desde Cache Reserve)0,36 $ por millón

Las purgas son gratis. Merece la pena con muchos ficheros grandes que se piden poco y un origen caro o lento.

Purgar la caché

Puedes purgar por URL (lo recomendado), por hostname, por cache tag, por prefijo o todo. Todas las opciones están en todos los planes, pero con límites distintos:

PurgaFreeProBusinessEnterprise
Por URL800 URL/s1500 URL/s1500 URL/s3000 URL/s
Hostname, tag, prefijo o todo5 peticiones/min5 peticiones/s10 peticiones/s50 peticiones/s

En vez de purgar, a menudo es mejor versionar los ficheros (app.3f9a.js o ?v=…): el fichero nuevo tiene otra clave y el antiguo caduca solo.

La Cache API de Workers

Desde un Worker puedes usar la caché directamente con la Cache API (caches.default), por ejemplo para cachear respuestas que genera el propio Worker. Este ejemplo simula una API lenta y guarda su respuesta:

// Simula una API lenta (por ejemplo, un tipo de cambio que se calcula en el origen)
async function calcularTipos(): Promise<Response> {
  await new Promise((r) => setTimeout(r, 300));
  return Response.json(
    { eurUsd: 1.17, generado: new Date().toISOString() },
    // max-age para el navegador; s-maxage para cachés compartidas como la de Cloudflare
    { headers: { "Cache-Control": "public, max-age=30, s-maxage=60" } },
  );
}

export default {
  async fetch(request, env, ctx): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname !== "/tipos") return new Response("No encontrado", { status: 404 });

    // La caché solo guarda GET; la clave es la URL (incluida la query string)
    const cache = caches.default;
    const clave = new Request(url.toString(), { method: "GET" });

    const enCache = await cache.match(clave);
    if (enCache) {
      const r = new Response(enCache.body, enCache);
      r.headers.set("X-Cache", "HIT");
      return r;
    }

    const respuesta = await calcularTipos();
    // Guardar en segundo plano sin retrasar la respuesta
    ctx.waitUntil(cache.put(clave, respuesta.clone()));

    const r = new Response(respuesta.body, respuesta);
    r.headers.set("X-Cache", "MISS");
    return r;
  },
} satisfies ExportedHandler<Env>;

En local, Wrangler simula la caché. Resumen de tres peticiones (código, tiempo, cabeceras relevantes y cuerpo):

$ curl -s -D - -w "%{http_code} %{time_total}s\n" localhost:8807/tipos
200 0.331360s
Cache-Control: public, max-age=30, s-maxage=60
X-Cache: MISS
{"eurUsd":1.17,"generado":"2026-10-02T22:48:29.433Z"}
$ curl -s -D - -w "%{http_code} %{time_total}s\n" localhost:8807/tipos
200 0.006209s
Cache-Control: public, max-age=30, s-maxage=60
X-Cache: HIT
{"eurUsd":1.17,"generado":"2026-10-02T22:48:29.433Z"}
$ curl -s -D - -w "%{http_code} %{time_total}s\n" localhost:8807/tipos?moneda=gbp
200 0.305726s
Cache-Control: public, max-age=30, s-maxage=60
X-Cache: MISS
{"eurUsd":1.17,"generado":"2026-10-02T22:48:29.768Z"}

La primera tarda 330 ms (la API lenta), la segunda 6 ms y devuelve el mismo generado, y otra query string vuelve a ser un fallo de caché. Lo que dice la documentación y conviene tener presente:

  • La Cache API es local al centro de datos: lo que guardas en Madrid no existe en París.
  • cache.put no es compatible con Tiered Cache. Si quieres que el Worker aproveche la caché por niveles, haz fetch() al origen con opciones de caché en lugar de usar la Cache API.
  • Las respuestas con Set-Cookie no se guardan nunca; quita la cabecera o usa Cache-Control: private=Set-Cookie.
  • cache.put devuelve un 413 si Cache-Control impide cachear o la respuesta es demasiado grande.
  • En el editor del panel y en el Playground, la Cache API no tiene efecto.

Errores comunes (y cómo evitarlos)

  • Esperar que el HTML se cachee solo. No se cachea por defecto; necesitas una Cache Rule.
  • Cachear páginas personalizadas. Excluye las rutas con sesión o salta la caché cuando haya cookie.
  • Set-Cookie en ficheros estáticos. Un framework que añade una cookie a todo hace que nada se cachee. Revisa las cabeceras del origen.
  • Purgar todo en cada despliegue. En Free solo tienes 5 peticiones de purga masiva por minuto, y la caché vacía dispara la carga del origen. Versiona los ficheros.
  • Query strings aleatorias. Un parámetro de seguimiento (?utm_…) crea una entrada nueva por valor. Ignóralo en la clave de caché con una Cache Rule.
  • No activar Tiered Cache. Es gratis y reduce mucho las peticiones al origen.
  • Fiarse de la Cache API como caché global. Es por centro de datos; para datos compartidos, usa KV.

Cuándo usar cada cosa

Siempre: deja que el CDN cachee los estáticos, versiónalos y activa Tiered Cache Smart. Cache Rules cuando quieras cachear HTML o JSON públicos, ajustar TTL o limpiar la clave de caché. Cache Reserve si tienes mucho contenido de cola larga y un origen caro. Cache API cuando el contenido lo genera un Worker y merece la pena reutilizarlo en ese centro de datos.

Sigue leyendo