
# Por qué tu CDN no cachea nada: la cabecera que lo impide

Todos los ficheros estáticos de agentready.md volvían con la misma línea:

```
cf-cache-status: BYPASS
```

Tipografías, hojas de estilo, scripts, imágenes. No `MISS`, que significaría que el borde todavía no había visto el fichero y se queda con la copia que acaba de traer. `BYPASS` significa que el borde ha leído la respuesta, ha decidido que no puede guardarla y mañana decidirá lo mismo. Cada visitante se bajaba todo desde el origen, y habría seguido haciéndolo para siempre.

## Las cuatro opciones que no eran el problema

- **Development Mode**: desactivado. Apaga la caché del borde durante tres horas y se queda encendido mucho más de lo que debería, así que míralo primero.
- **Caching Level**: Standard.
- **Page Rules**: ninguna ponía Cache Level en Bypass.
- **Browser Cache TTL**: un valor razonable.

Todo lo que el panel tenía que decir sobre caché decía *guarda esto*. El borde no guardaba nada.

## La causa estaba en la respuesta, no en el panel

Aparece en cuanto lees el bloque de cabeceras entero en vez de la línea que ibas buscando:

```bash
curl -sI https://agentready.md/fonts/ibm-plex-sans-latin.woff2
```

```
HTTP/2 200
content-type: font/woff2
cache-control: public, max-age=31536000, immutable
set-cookie: _csrf=8f3c…; Path=/; SameSite=Strict
cf-cache-status: BYPASS
```

La tercera línea pide un año. La cuarta explica por qué nadie se lo concede.

**Una respuesta que lleva `Set-Cookie` no se guarda en una caché compartida.** Una cookie es de un visitante; guardar esa respuesta y dársela al siguiente es entregarle la cookie de otro. Todos los CDN se niegan. Cloudflare le pone nombre a la negativa, `BYPASS`, y otros directamente no guardan el objeto y no te lo cuentan.

La cookie era nuestra. Un middleware generaba un token CSRF en cada petición y lo adjuntaba para que cualquier formulario de la web se lo encontrara ya puesto. En «cada petición» entraba la de una tipografía woff2, un fichero que no pinta formularios, no lee ningún token y no ejecuta código.

## El arreglo

El CSRF se queda como está. Lo que se quita es el token para lo que no puede usarlo:

```js
const ASSET_PREFIXES = ['/css/', '/js/', '/fonts/', '/images/'];
const COOKIELESS_FILES = new Set([
  '/favicon.ico', '/badge.js', '/robots.txt', '/sitemap.xml', '/llms.txt',
]);

function servesWithoutCookie(path) {
  return COOKIELESS_FILES.has(path)
    || ASSET_PREFIXES.some((prefix) => path.startsWith(prefix))
    || path.endsWith('.md');
}
```

Compara rutas, no extensiones. Algunas de las respuestas que más piden caché no salen del disco sino de una plantilla, como el gemelo en Markdown de cada página o nuestras insignias en SVG, y tampoco pintan formularios.

Si la cookie no la has puesto tú, mira la capa de sesión. Muchos frameworks abren sesión, y mandan su cookie, en cualquier petición que pase por el middleware, aunque nunca se guarde nada dentro.

## Cómo diagnosticarlo en cualquier stack

Mira un asset, no la portada. Una portada puede no ser cacheable por motivos legítimos y no te dice nada de los treinta ficheros que arrastra detrás.

```bash
for u in / /css/app.css /js/app.js /fonts/ibm-plex-sans-latin.woff2; do
  echo "== $u"
  curl -sI "https://ejemplo.com$u" \
    | grep -iE 'cache-control|set-cookie|cf-cache-status|x-cache|^age'
done
```

Pide la misma URL dos veces. La segunda debería ser un acierto: `cf-cache-status: HIT`, `x-cache: Hit from cloudfront` o una cabecera `age:` que sube en Varnish y en nginx. Si la segunda también falla, el objeto no se está guardando y el motivo está en las cabeceras que acabas de imprimir.

## Los otros sospechosos de siempre

`Cache-Control: private`: solo navegadores, nunca una caché compartida. `no-store`: nadie, ni el navegador. Y `no-cache` no significa «no guardes», significa revalida antes de cada uso, que sigue siendo un viaje de ida y vuelta por fichero.

`Vary: *` deja la respuesta fuera de la caché sin más. `Vary: Cookie` es casi igual de malo, porque la clave de caché pasa a incluir un valor distinto para cada visitante y así no se comparte nada. `Vary: User-Agent` parte un fichero en miles de variantes. `Accept-Encoding` es el que sí pinta ahí.

Las cadenas de consulta. Casi todas las cachés usan la URL entera como clave, así que cada variante con `?utm_source=` o `?fbclid=` es un objeto aparte que se pide al origen. Limpia o normaliza los parámetros de campaña en el borde.

Y el valor por defecto del framework, que no llega a bypass pero cuesta casi lo mismo: `max-age=0` en cada fichero estático son tantas peticiones de revalidación como assets tenga cada navegación.

## Una política de caché que se sostiene

La regla es lo que promete la URL, no lo que es el fichero.

Una URL con un hash del contenido (`app.css?v=9f2ac41b`, `app.9f2ac41b.css`) es una dirección de contenido: si cambia el fichero, cambia la URL. Esas van con `public, max-age=31536000, immutable` y no hay nada que purgar nunca. El mismo fichero sin el token se queda en una hora, porque el token es la promesa y no se cachea sobre una promesa que nadie ha hecho. Un logo o una portada social, que se reemplazan en su sitio, aguantan una semana.

Queda la excepción, la que casi todo el mundo falla porque el fichero parece lo más cacheable que tiene: cualquier cosa incrustada en páginas ajenas, con una URL que no puedes versionar. Servimos `badge.js` con una hora solo por eso. Se ejecuta dentro de webs donde no controlamos ni el HTML ni la URL, así que un año de `immutable` sería un fallo al que no podríamos llegar durante un año.

## Si sirves ficheros con @fastify/static

Dos detalles que nos costaron una tarde. Manda `Cache-Control: public, max-age=0` por defecto. Y si pones tú la cabecera con `setHeaders`, pasa también `cacheControl: false` o la librería construye la suya a partir de su `maxAge` y machaca la tuya sin avisar, dejando algo que parece puesto a propósito:

```js
app.register(fastifyStatic, {
  root: join(__dirname, 'public'),
  cacheControl: false,           // o setHeaders queda machacado
  setHeaders: setStaticCacheHeaders,
});
```

Antes de tocar una sola opción de caché, léete una respuesta entera. La cabecera que rompe la caché casi nunca es la que lleva «cache» en el nombre.

[Analiza tu web](/es) · [Checklist de preparación para IA](/es/tools/ai-readiness-checklist) · [¿Están permitidos los bots?](/es/tools/ai-crawlers-checker)
