Encabezado Cache-Control: cuánto tiempo se guarda un archivo en caché

Cache-Control es el encabezado HTTP que indica a navegadores y CDN si pueden guardar una respuesta y por cuántos segundos. Para archivos con hash en el nombre, usa public, max-age=31536000, immutable.

Actualizado el 6 min de lectura

Objetivos de aprendizaje

Después de leer este artículo podrás:

  • Entender qué hacen realmente max-age, no-cache, no-store e immutable
  • Elegir el Cache-Control correcto para HTML, assets, imágenes y respuestas de API
  • Configurar el encabezado en un servidor Node.js y verificar el resultado con curl
En esta página

Cada vez que abres un sitio, el navegador decide si descarga los archivos otra vez o reutiliza una copia guardada. Quien manda en esa decisión es el servidor, mediante el encabezado Cache-Control. Bien configurado, hace el sitio más rápido y reduce la carga del servidor. Mal configurado, hace que el visitante vea una versión vieja durante días.

Si la CDN todavía es una caja negra para ti, lee antes qué es una CDN.

frescavencidareemplazadaNavegadorCDNOrigenmax-ageno consultadoETag304200copia nueva
Fresca, la copia sale de la caché; vencida, la caché revalida por ETag y recibe un 304 o el archivo nuevo con un 200.

¿Qué hace Cache-Control?#

Cache-Control es un encabezado de respuesta (y también de solicitud) de HTTP. Trae una lista de directivas separadas por coma que valen para todas las cachés del camino: la del navegador (privada, de un solo usuario) y las compartidas, como CDN y proxies inversos. Las reglas están en la RFC 9111.

http
HTTP/2 200
content-type: text/css
cache-control: public, max-age=31536000, immutable

Sin ninguna información de frescura, la caché puede estimar un plazo por su cuenta (caché heurística), normalmente una fracción del tiempo desde la última modificación. Por eso la regla es: declara lo que quieres, no dejes la decisión a la caché.

¿Qué directivas importan en el día a día?#

  • max-age=N: la respuesta se considera fresca durante N segundos, contados desde que el servidor la generó. Fresca significa que la caché la entrega sin preguntar nada al servidor.
  • s-maxage=N: lo mismo, pero solo para cachés compartidas (CDN). Sobrescribe el max-age en ellas, y el navegador lo ignora.
  • no-cache: puede guardarse, pero debe revalidarse con el servidor antes de cada reutilización. No significa "no uses caché".
  • no-store: ninguna caché puede guardar la respuesta. Úsalo para datos sensibles.
  • public y private: private limita el almacenamiento a la caché del navegador del usuario; public permite cachés compartidas, incluso para solicitudes con Authorization.
  • immutable: el contenido no cambia mientras esté fresco, así que el navegador ni siquiera revalida al recargar. Solo tiene sentido cuando la URL cambia junto con el contenido.
  • must-revalidate: una vez vencida, la respuesta nunca se sirve sin revalidar. max-age=0, must-revalidate equivale en la práctica a no-cache.
  • stale-while-revalidate=N: una vez vencida, la caché puede entregar la copia vieja durante N segundos más mientras busca la nueva en segundo plano.
  • stale-if-error=N: si el servidor responde con error (500, 502, 503 o 504), la caché puede entregar la copia vieja hasta por N segundos.

El error más común es creer que no-cache impide el almacenamiento. Quien lo impide es no-store.

¿Cómo funciona la revalidación?#

Cuando la copia vence, la caché no necesita descargar el archivo entero otra vez. Pregunta al servidor si algo cambió, enviando If-None-Match con el ETag que recibió (o If-Modified-Since con el Last-Modified). Si nada cambió, el servidor responde 304 Not Modified, sin cuerpo, y la copia vuelve a ser fresca. Si cambió, responde 200 con el archivo nuevo.

Ese es el mecanismo detrás de no-cache: el navegador siempre hace un viaje al servidor, pero casi siempre recibe solo un 304 de pocos bytes.

¿Qué Cache-Control usar para cada tipo de archivo?#

Tipo de respuesta Encabezado Por qué
JS, CSS e imágenes con hash en el nombre (app.3f9a1c.js) public, max-age=31536000, immutable La URL cambia cuando cambia el contenido, así que puede quedarse un año
index.html y páginas que apuntan a los assets no-cache Siempre revalida y pasa a apuntar a los archivos nuevos
Imagen o archivo sin hash en el nombre public, max-age=86400 (o menos) Acepta quedar desactualizado por un tiempo conocido
Respuesta de API personalizada por usuario private, no-cache Nunca va a una caché compartida
Dato sensible (token, estado de cuenta) no-store No puede quedar guardado en ningún lugar
Contenido público que puede atrasarse unos minutos max-age=60, stale-while-revalidate=300 Respuesta instantánea y actualización en segundo plano
TTL distinto en la CDN y en el navegador max-age=60, s-maxage=3600 La CDN guarda 1 hora, el navegador 1 minuto

La lógica de las dos primeras filas es la más importante: el HTML nunca queda mucho tiempo en caché y los archivos versionados quedan un año. Cada deploy genera nombres nuevos, el HTML nuevo apunta a ellos y nadie ve una versión vieja. Herramientas como Vite ya ponen el hash en el nombre. Más en qué es un sitio estático.

¿Cómo configurarlo en Node.js (Express)?#

express.static acepta las opciones maxAge e immutable. Con setHeaders cambias el encabezado según el archivo. Ejemplo con Express 5:

js
import express from "express";

const app = express();

// Archivos con hash en el nombre: un año, inmutables.
app.use(
  "/assets",
  express.static("dist/assets", { maxAge: "1y", immutable: true }),
);

// El resto (index.html, etc.): siempre revalida.
app.use(
  express.static("dist", {
    setHeaders(res) {
      res.setHeader("Cache-Control", "no-cache");
    },
  }),
);

// API con datos del usuario: nunca en caché compartida.
app.get("/api/me", (req, res) => {
  res.set("Cache-Control", "private, no-cache");
  res.json({ name: "Ana" });
});

app.listen(process.env.PORT ?? 3000);

El primer app.use responde con Cache-Control: public, max-age=31536000, immutable. En otros servidores la idea es la misma: en Nginx, usa add_header Cache-Control "public, max-age=31536000, immutable"; dentro de location /assets/.

¿Cómo verificar que funcionó?#

Pide solo los encabezados con curl -I y revisa el cache-control:

bash
curl -sI https://tu-dominio.com/assets/app.3f9a1c.js | grep -i -E "cache-control|^age|etag"

Si hay una CDN en el camino, un encabezado age mayor que cero indica que la respuesta vino de su caché. En el navegador, la pestaña Red (Network) de las herramientas de desarrollo muestra "(disk cache)" o "(memory cache)" cuando se reutilizó la copia local. Ojo: recargar con F5 o Ctrl+Shift+R envía max-age=0 o no-cache en la solicitud, así que prueba abriendo la URL en una pestaña nueva.

¿Qué errores perjudican más?#

  • Max-age largo en un archivo sin hash. El visitante se queda con el CSS viejo hasta que venza. Usa plazos largos solo con URL versionada.
  • Max-age largo en el HTML. Aunque los assets sean nuevos, el navegador no busca el HTML que apunta a ellos.
  • Usar no-cache esperando bloquear la caché. Para eso es no-store.
  • Cachear una respuesta personalizada como public. Una CDN puede entregar los datos de un usuario a otro. Usa private.
  • Mezclar directivas en conflicto. private, no-cache, no-store, max-age=0, must-revalidate equivale a no-store; elige una sola intención.
  • Olvidar la caché de la CDN después de corregir. Cambiar el encabezado no limpia lo que ya se guardó; hace falta un purge o esperar a que venza.

Cómo hacerlo en Square Cloud#

En Square Cloud, todos los planes incluyen una CDN global que respeta el Cache-Control que envía tu aplicación. La receta de esta guía funciona tal cual: hash en el nombre de los assets con max-age de un año, no-cache en el HTML y private o no-store en lo que pertenece a un usuario. Defines los encabezados en el código, como en el ejemplo con Express, haces el deploy y lo compruebas con curl -I en el subdominio *.squareweb.app que toda aplicación web recibe con HTTPS.

Cuando un archivo equivocado ya quedó en caché, usa la purga de caché, disponible en el subdominio squareweb.app y en un dominio personalizado (desde el plan Standard), desde el panel o la extensión de VS Code. Borra la caché de todo el host, así que úsala como corrección puntual y deja que las URLs versionadas se encarguen de los deploys del día a día.

Para los archivos que suben tus usuarios, Blob Storage permite definir el Cache-Control por objeto y reglas de caché por carpeta, sin pasar por tu aplicación. Consulta la purga en dominios personalizados y la CDN en red.

Red

Dominio propio, TLS automático y CDN global para tu app

Más información

Preguntas frecuentes

¿Cuál es la diferencia entre no-cache y no-store?

no-cache permite guardar la respuesta, pero obliga a revalidar con el servidor antes de reutilizarla. no-store prohíbe que cualquier caché la guarde. Para datos sensibles, usa no-store.

¿Cuál es el valor máximo de max-age?

No hay un límite práctico que debas memorizar: 31536000 segundos (un año) es el valor habitual para archivos versionados. Los valores negativos o decimales tienen comportamiento indefinido.

¿Qué es s-maxage?

Es un tiempo de frescura válido solo para cachés compartidas, como CDN y proxies. Sobrescribe el max-age en ellas, y el navegador lo ignora.

¿Por qué el navegador no toma la versión nueva de mi archivo?

Casi siempre el archivo se sirvió con un max-age largo y la URL no cambió. Renombra el archivo con un hash del contenido o reduce el max-age de lo que cambia a menudo, como el HTML.

Siguiente artículoCómo usar un dominio personalizado en tu appPara usar un dominio personalizado, añádelo en tu hosting y crea en el DNS los registros que te indique. Pasos, www, HTTPS y errores comunes.

Más en Redes y dominios

Ver los 27 artículos