Volver al Blog

El bug de caché que nadie ve (hasta que es demasiado tarde)

Por qué tus usuarios pueden seguir viendo una versión rota de tu sitio semanas después de que la arreglaste, y cómo configurar Cache-Control para que esto no te pase.

El bug de caché que nadie ve (hasta que es demasiado tarde)

Hace unos días subí un cambio de CSS a un sitio en producción. El deploy terminó sin errores, el archivo en el servidor tenía el contenido correcto, hasta un curl directo lo confirmaba. Y sin embargo, varios visitantes seguían viendo el diseño viejo, roto, minutos y horas después.

No era un problema de build. No era un problema de DNS. Era Cache-Control mal configurado en un archivo con nombre fijo, y es uno de los bugs más silenciosos que existen porque el servidor nunca miente sobre lo que tiene — el problema está en lo que le dijiste al navegador que hiciera con esa respuesta.

El síntoma

Todo apuntaba a que el deploy no había funcionado:

La pista estaba en la respuesta HTTP, no en el archivo:

curl -sI https://ejemplo.com/style.css | grep -i cache-control
# Cache-Control: public, max-age=2678400, must-revalidate

2678400 segundos son 31 días. Le estaba diciendo a cada navegador (y a cualquier caché intermedia) que no volviera a preguntar por ese archivo durante un mes entero. El servidor tenía la versión nueva desde el minuto uno; nadie se molestaba en pedirla de nuevo.

Por qué pasa esto

La mayoría de los frameworks de build modernos (Vite, Webpack, Next.js) resuelven este problema para los archivos que ellos generan: le agregan un hash al nombre basado en el contenido. Si el contenido cambia, el nombre cambia:

main-a1b2c3.js  main-f9e8d7.js

Como la URL es distinta, no hay ambigüedad posible: cachear ese archivo "para siempre" (max-age=31536000, immutable) es correcto y deseable, porque nunca vas a reusar esa URL para contenido distinto.

El problema aparece con los archivos que no pasan por ese pipeline de hasheo: un style.css servido tal cual desde una carpeta pública, un robots.txt, un manifest.json, o cualquier asset que referencies con una ruta fija en tu HTML. Si tu hosting (o un plugin, o un _headers genérico) les aplica el mismo Cache-Control largo pensado para assets hasheados, quedan atrapados: misma URL, contenido distinto, caché que no lo sabe.

La regla práctica

No es "cachear más es mejor" ni "cachear menos es más seguro". Es hacer que la política de caché coincida con si la URL puede cambiar de contenido sin cambiar de nombre:

Tipo de archivo Cache-Control recomendado
Assets con hash en el nombre (app-a1b2c3.js) public, max-age=31536000, immutable
HTML (siempre debe reflejar el último deploy) public, max-age=0, must-revalidate
Archivos con nombre fijo (style.css, favicon.ico) public, max-age=0, must-revalidate o un TTL corto

must-revalidate no significa "no cachear" — significa "podés guardarlo, pero antes de usarlo, preguntale al servidor si sigue vigente" (una petición condicional con ETag/If-None-Match, que si no cambió responde 304 Not Modified casi sin costo). Es el punto medio correcto para archivos de nombre fijo que sí cambian de vez en cuando.

Arreglándolo en Cloudflare Pages

Si usas Cloudflare Pages, el archivo _headers en tu carpeta public/ te deja definir reglas por ruta:

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY

/style.css
  Cache-Control: public, max-age=0, must-revalidate

/main.js
  Cache-Control: public, max-age=0, must-revalidate

Las reglas más específicas (/style.css) se combinan con las genéricas (/*), así que no perdés los headers de seguridad que ya tenías. Cloudflare aplica la lista completa en cada respuesta que matchea.

Cómo detectarlo antes de que te avisen los usuarios

Un curl -I a tus assets estáticos con nombre fijo es la forma más rápida de auditar esto:

for f in style.css main.js robots.txt manifest.json; do
  echo "== $f =="
  curl -sI "https://tu-sitio.com/$f" | grep -i cache-control
done

Si ves un max-age de varios días en algo que no tiene hash en el nombre, es candidato a revisión. No hace falta que sea max-age=0 en todos los casos — un TTL de unos minutos u horas puede tener sentido para reducir carga en archivos que cambian poco — pero el número tiene que ser una decisión consciente, no el default que vino con la plataforma.

La lección

Cachear agresivamente es, la mayoría del tiempo, la optimización correcta. El error no fue cachear — fue cachear una URL mutable como si fuera inmutable. La próxima vez que un cambio "no se vea" en producción a pesar de que el deploy fue exitoso, antes de sospechar del build, mirá la respuesta HTTP del archivo en cuestión. La caché no se equivoca; hace exactamente lo que le pediste.

¿Te sirvió este artículo?
Compartir en LinkedIn Compartir en X
Servicios Profesionales

¿Necesitas ayuda con tu proyecto?

Soluciones digitales para empresas chilenas