Cosas que Nunca Deberías Cachear
El caché es una de las mejores herramientas para el rendimiento en el frontend. Bien implementado, elimina solicitudes de red redundantes, reduce la carga del servidor y hace que tu aplicación se sienta instantánea. Mal implementado, filtra datos privados, sirve contenido autenticado desactualizado o deja a los usuarios atrapados en estados de página rotos de los que no pueden escapar.
La regla es más sencilla de lo que la mayoría de los artículos sugieren: los assets estáticos, versionados y públicos casi siempre son seguros para cachear de forma agresiva. Todo lo demás debe tratarse como inseguro por defecto.
Antes de entrar en lo que no se debe cachear, conviene ser precisos sobre dónde ocurre el caché, ya que estas capas se comportan de manera muy diferente.
Puntos Clave
- Las capas de caché (HTTP, CDN, Service Worker, bfcache, Web Storage) tienen alcances y características de seguridad distintos; tratarlas de forma indistinta genera errores reales.
no-storeimpide el almacenamiento por completo, mientras queno-cachesolo fuerza la revalidación antes de reutilizar.- Las respuestas autenticadas o específicas de un usuario deben usar
Cache-Control: private, no-storepara evitar que los cachés compartidos filtren datos entre usuarios. - Nunca almacenes JWTs, refresh tokens ni claves de API en
localStorageoIndexedDB; utiliza cookiesHttpOnly. - Usa
no-storepor defecto siempre que una copia desactualizada o compartida pueda causar un problema de seguridad o de integridad.
Las Capas de Caché No Son Intercambiables
La mayoría de los errores de caché comienzan por tratar estas capas como si fueran lo mismo:
- Caché HTTP/navegador — Controlado por las cabeceras
Cache-Controlsegún la RFC 9111. Compartido entre pestañas y persistente entre sesiones. - Caché CDN/compartido — Se sitúa entre tus usuarios y tu servidor de origen. Servicios como Cloudflare CDN cachean respuestas para todos los usuarios, no solo para uno.
- Service Worker Cache API — Un caché programable que controlas completamente desde JavaScript. Persiste hasta que se borra de forma explícita.
- bfcache (caché de navegación hacia atrás/adelante) — Una instantánea en memoria de una página completa que el navegador utiliza al navegar hacia atrás. Es independiente del caché HTTP.
localStorage/sessionStorage— Almacenamiento clave-valor. Sin gestión de expiración y completamente accesible desde JavaScript.IndexedDB— Almacenamiento persistente estructurado. Presenta la misma superficie de exposición a ataques XSS quelocalStorage.
Cada capa tiene características distintas de persistencia, alcance y seguridad. Confundirlas genera errores reales.
Una Nota Rápida sobre la Semántica de Cache-Control
Esta distinción se malinterpreta con frecuencia, por lo que vale la pena aclararla:
no-storesignifica que el caché no debe almacenar la respuesta en absoluto.no-cacheno significa “no cachear”. Significa que la copia cacheada debe revalidarse con el servidor antes de reutilizarse.
Si quieres que una respuesta nunca se almacene en ningún lugar, usa no-store. Si quieres garantizar la frescura en cada uso pero aun así aprovechar la eficiencia de las solicitudes condicionales (ETags, 304s), usa no-cache.
Qué Nunca Deberías Cachear
Respuestas de API Autenticadas y HTML Específico de Usuario
Cualquier respuesta que varíe según la identidad del usuario —HTML de dashboards, páginas de cuenta, respuestas de API con datos de perfil— no debe almacenarse en un caché compartido. Un patrón habitual es:
Cache-Control: private, no-store
no-store impide que los cachés HTTP normales almacenen la respuesta, mientras que private indica explícitamente a los cachés compartidos como los CDN que no la almacenen para múltiples usuarios.
Sin private, un CDN como Cloudflare puede cachear una respuesta devuelta para el Usuario A y servirla al Usuario B. Esto ha causado incidentes reales de exposición de datos.
Cualquier Cosa Dentro de un Service Worker que Requiera Autenticación
La Service Worker Cache API es, en la práctica, un proxy de red programable. Un service worker intercepta cada solicitud fetch dentro de su ámbito. Si cacheas de forma indiscriminada respuestas de API autenticadas o HTML específico de usuario, esos datos persisten en el almacenamiento de la Cache API —potencialmente entre sesiones— y el service worker puede acceder a ellos independientemente del estado de autenticación.
// ❌ No hagas esto
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).then(response => {
return caches.open('v1').then(cache => {
cache.put(event.request, response.clone());
return response;
});
})
);
});
Este patrón cachea todo de forma indiscriminada. Excluye las rutas autenticadas de forma explícita:
// ✅ Solo cachea assets públicos y versionados
const CACHEABLE = ['/shell.html', '/app-abc123.js', '/styles-f9c.css'];
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.origin !== self.location.origin) return; // ignorar cross-origin
if (!CACHEABLE.includes(url.pathname)) return; // dejar que vaya a la red
event.respondWith(
caches.match(event.request).then(r => r || fetch(event.request))
);
});
Secretos en localStorage o IndexedDB
La Guía de Seguridad HTML5 de OWASP es explícita al respecto: almacenar JWTs, refresh tokens, claves de API o tokens de restablecimiento de contraseña en localStorage o IndexedDB es arriesgado, ya que cualquier vulnerabilidad XSS en tu dominio puede leerlos. Estos mecanismos de almacenamiento no tienen expiración integrada, no tienen un equivalente a HttpOnly y no están aislados de scripts inyectados.
Utiliza cookies con los atributos HttpOnly, Secure y SameSite para los tokens siempre que sea posible. Si debes usar localStorage, asume conscientemente ese riesgo.
Discover how at OpenReplay.com.
Respuestas Específicas de Usuario en la Capa CDN
Un error frecuente con Cloudflare CDN: olvidar la directiva private en respuestas que incluyen datos de usuario, o configurar incorrectamente Vary.
# ❌ Falta private — el CDN puede cachear esto para todos los usuarios
Cache-Control: max-age=300
Content-Type: application/json
# ✅ Correcto
Cache-Control: private, no-store
Si tu respuesta varía legítimamente según una cabecera de solicitud (como Accept-Language o Authorization), debes incluir una cabecera Vary correcta. Sin ella, un CDN podría servir una respuesta cacheada en francés a un usuario de habla inglesa o, peor aún, una respuesta autenticada cacheada a un usuario no autenticado.
También hay que vigilar el envenenamiento de caché mediante parámetros no incluidos en la clave de caché: si tu CDN cachea en función de la URL pero tu aplicación lee un parámetro de consulta o una cabecera no validada para construir la respuesta, un atacante puede envenenar el caché con una solicitud manipulada.
Páginas que No Deberían Sobrevivir al bfcache
Los navegadores basados en Chromium, en sus versiones recientes, han aumentado su disposición a usar bfcache incluso en páginas que envían Cache-Control: no-store. Este comportamiento depende del navegador y no es consistente entre Chrome, Firefox y Safari. caniuse muestra una amplia compatibilidad con bfcache en los navegadores modernos, pero la interacción con no-store varía entre motores y versiones.
Si tienes páginas en las que restaurar una instantánea en memoria desactualizada es genuinamente inseguro —como una página que muestra la confirmación de un pago único o un estado sensible de sesión— usa el evento pageshow para detectar restauraciones desde bfcache y recargar o revalidar:
window.addEventListener('pageshow', event => {
if (event.persisted) {
// La página fue restaurada desde bfcache
window.location.reload();
}
});
No asumas que no-store deshabilita el bfcache de forma universal en todos los navegadores.
El Modelo Mental por Defecto
Si no estás seguro de si algo debería cachearse, pregúntate: ¿podría servir una copia desactualizada o compartida de esto causar un problema de seguridad o de integridad? Si la respuesta es sí, usa Cache-Control: no-store por defecto y trabaja a partir de ahí.
Los assets estáticos con URLs que incluyen hash del contenido —tus bundles de JavaScript, archivos CSS, imágenes— son seguros para cachear durante un año con max-age=31536000, immutable. Todo lo que esté vinculado a una sesión de usuario, un estado de autenticación o una operación sensible, no lo es.
Conclusión
El caché agresivo es una funcionalidad. El caché accidental de los elementos incorrectos es una vulnerabilidad. La clave está en saber qué capa estás usando, qué garantiza realmente cada directiva de Cache-Control y qué categorías de datos nunca deben salir del control estricto del servidor de origen. Trata todo lo vinculado a un usuario, una sesión o un secreto como no cacheable por defecto, y reserva el caché agresivo para los assets estáticos y versionados para los que fue diseñado.
Preguntas Frecuentes
Es posible, pero no recomendable. Cualquier vulnerabilidad XSS en tu dominio otorga a un atacante acceso de lectura completo a localStorage, lo que significa que puede robar el token y suplantar al usuario. Las cookies con los atributos HttpOnly, Secure y SameSite son más seguras porque JavaScript no puede leerlas directamente. Si debes usar localStorage, estás asumiendo el riesgo de XSS y deberías invertir considerablemente en CSP y en la sanitización de entradas.
no-store prohíbe que los cachés HTTP normales almacenen la respuesta para reutilizarla. no-cache permite que la respuesta se almacene, pero exige revalidación con el servidor de origen antes de cada reutilización, generalmente mediante solicitudes condicionales con ETag o Last-Modified. Usa no-store para datos sensibles y no-cache cuando quieras garantías de frescura pero aun así quieras beneficiarte de las respuestas 304 Not Modified.
Generalmente porque la respuesta carece de la directiva private o tiene una cabecera Vary incorrecta. Sin private, un caché compartido trata la respuesta como cacheable para todos. Sin Vary en cabeceras como Authorization o Cookie, el CDN ignora esas cabeceras al generar las claves de caché y puede servir la respuesta del Usuario A al Usuario B. Envía siempre Cache-Control: private, no-store en las respuestas específicas de usuario.
No existe una cabecera fiable y compatible con todos los navegadores que deshabilite el bfcache. Cache-Control: no-store funciona en algunos navegadores, pero no de forma consistente. El enfoque fiable es escuchar el evento pageshow y comprobar event.persisted. Si es true, la página fue restaurada desde bfcache y puedes forzar una recarga o volver a obtener el estado sensible. Esto es esencial en páginas de confirmación de pago y en páginas posteriores al cierre de sesión.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.