Cache-Control es un encabezado HTTP que define las reglas de almacenamiento en caché de los recursos en el cliente, los servidores proxy y las CDN mediante un conjunto de directivas. A diferencia del obsoleto encabezado Expires, Cache-Control admite docenas de combinaciones: max-age establece el tiempo de vida en segundos, private y public controlan la disponibilidad de la caché, no-cache y no-store — verificación forzada. Según Google Web Dev (2025), una configuración correcta de Cache-Control puede reducir el tiempo de carga de las páginas en un 50-80% para las visitas repetidas. Esto hace que el encabezado sea críticamente importante para el rendimiento de aplicaciones web y móviles.
Puntos clave
Cache-Control es un encabezado HTTP, estandarizado en HTTP/1.1 (RFC 7234), que permite al servidor especificar cómo y durante cuánto tiempo los clientes, proxies y CDN pueden almacenar en caché la respuesta. A diferencia de Expires (HTTP/1.0), Cache-Control utiliza directivas — comandos de texto combinados con comas: Cache-Control: public, max-age=3600, must-revalidate. El encabezado proporciona un control detallado sobre cada eslabón de la cadena de caché.
El almacenamiento en caché es uno de los mecanismos fundamentales del rendimiento de aplicaciones web y móviles. Sin él, cada solicitud del usuario iría directamente al servidor, provocando una carga excesiva y latencia. Cache-Control define tres niveles de caché: navegador/aplicación (caché privada), servidores proxy (caché compartida) y CDN (caché distribuida). Cada nivel interpreta las directivas de manera diferente.
La configuración incorrecta de Cache-Control es una de las causas más comunes de problemas de rendimiento. Un almacenamiento en caché demasiado agresivo hace que los usuarios vean datos desactualizados. Un almacenamiento en caché demasiado débil provoca solicitudes excesivas al servidor y una carga lenta. Según Akamai (2025), optimizar Cache-Control para contenido estático reduce la carga del servidor en un 70-90% y mejora el tiempo de carga en un 40-60% para usuarios móviles.
Cache-Control apareció en HTTP/1.1 (RFC 2616, 1999) como reemplazo de Expires. Expires tenía un problema fundamental: usaba una fecha absoluta que dependía de las zonas horarias del servidor y el cliente. Cache-Control resolvió este problema al cambiar a tiempo relativo (max-age en segundos desde el momento en que se recibe la respuesta). Más tarde, en RFC 7234 (2014), se agregaron nuevas directivas: immutable para activos estáticos, stale-while-revalidate y stale-if-error para validación diferida.
Cache-Control incluye más de 10 directivas divididas en tres grupos: directivas de solicitud (cliente → servidor), directivas de respuesta (servidor → cliente) y extensiones. En la práctica, el desarrollo móvil utiliza 6-7 directivas de respuesta principales que cubren el 95% de los escenarios de caché. Veamos cada una con ejemplos y recomendaciones.
| Directiva | Significado | Ejemplo |
|---|---|---|
| max-age | Tiempo de vida en segundos desde el momento de la respuesta | max-age=3600 — 1 hora |
| s-maxage | max-age para caché compartida (proxy, CDN) | s-maxage=86400 — 1 día para CDN |
| public | Permite el almacenamiento en caché a todos (incluidos proxies) | public, max-age=3600 |
| private | Permite el caché solo para el navegador/aplicación | private, max-age=600 |
| no-cache | No usar sin validación (304 requerido) | no-cache |
| no-store | Prohibir completamente el almacenamiento en caché | no-store |
| must-revalidate | Después de max-age, debe revalidar con el origen | max-age=3600, must-revalidate |
| immutable | El recurso no cambiará (para activos estáticos versionados) | max-age=31536000, immutable |
max-age es la directiva más importante. Prohíbe al cliente realizar una solicitud al servidor durante el tiempo especificado. Para activos estáticos (CSS, JS, imágenes), max-age generalmente se establece de 1 día a 1 año. Para respuestas de API — de 0 segundos (datos siempre frescos) a 5-10 minutos (datos de referencia). s-maxage permite establecer diferentes tiempos de vida para CDN y navegador: la CDN almacena una copia durante 1 día, el navegador durante 1 hora.
Estas dos directivas a menudo se confunden. no-cache no prohíbe el almacenamiento en caché — requiere validar la copia almacenada en caché en cada uso mediante una solicitud condicional (If-Modified-Since o If-None-Match). Si el servidor responde con 304 — el cliente usa la caché. Si 200 — la actualiza. no-store, por otro lado, prohíbe completamente guardar la respuesta en cualquier caché, incluido el disco y la memoria. Use no-store solo para datos sensibles — tokens, datos de pago, documentos personales.
El encabezado Expires (HTTP/1.0) también especifica el tiempo de vida del recurso pero usa una fecha absoluta: Expires: Thu, 03 Jul 2026 12:00:00 GMT. Cache-Control max-age usa tiempo relativo desde el momento de la respuesta. La diferencia es crítica para sistemas distribuidos: si el servidor y el cliente están en diferentes zonas horarias, Expires puede interpretarse incorrectamente. Cache-Control no tiene este problema — 3600 segundos siempre son 3600 segundos.
Cuando ambos encabezados están presentes, Cache-Control tiene prioridad sobre Expires. Esto está definido en RFC 7234: “Si una respuesta incluye un campo Cache-Control con la directiva max-age, el destinatario DEBE ignorar el campo Expires.” En la práctica, se recomienda no devolver Expires en absoluto para clientes modernos, ya que Cache-Control cubre todos los escenarios de Expires. Sin embargo, para compatibilidad con proxies y navegadores antiguos, se pueden devolver ambos encabezados.
Expires ha sobrevivido principalmente para contenido estático en Nginx y Apache — estos servidores agregan automáticamente ambos encabezados. Si su proyecto encuentra Expires sin Cache-Control, reemplácelo por Cache-Control con max-age: la precisión del control de caché mejora y se elimina la dependencia de la zona horaria. Para la migración, basta con configurar el servidor para que agregue Cache-Control en lugar de Expires.
# Nginx: Cache-Control para archivos estáticos
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable, max-age=2592000";
}
# Diferentes políticas para diferentes tipos de contenido
location /api/config {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
location /api/static-data {
expires 5m;
add_header Cache-Control "public, max-age=300";
}
En la configuración de Nginx, los archivos estáticos (CSS, JS, imágenes) se configuran con Cache-Control durante 30 días con el atributo immutable — este atributo le indica al navegador que el recurso nunca cambia en esta URL (versionado mediante hash en el nombre del archivo). Los endpoints de API usan no-cache para datos dinámicos y public con un max-age corto para datos de referencia — listas solicitadas con frecuencia y que raramente cambian.
En las aplicaciones móviles, Cache-Control juega un papel especial debido a las limitaciones de las redes móviles: alta latencia, conexión inestable, límites de tráfico. Un almacenamiento en caché adecuado permite mostrar los datos al usuario al instante, incluso sin conexión, y actualizarlos en segundo plano. OkHttp en Android y URLSession en iOS tienen sistemas de caché integrados que respetan Cache-Control.
OkHttp usa CacheInterceptor, que lee Cache-Control de la respuesta y gestiona automáticamente el almacenamiento en caché. Si el servidor devolvió Cache-Control: max-age=3600, OkHttp no realizará una solicitud al servidor durante una hora. Después de que expire max-age, OkHttp envía una solicitud condicional con If-Modified-Since e If-None-Match. Configuración de caché en OkHttp: OkHttpClient.Builder().cache(Cache(directory, maxSize)).
fun createCachedClient(cacheDir: File): OkHttpClient {
return OkHttpClient.Builder()
.cache(Cache(cacheDir, 10L * 1024 * 1024))
.addNetworkInterceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control",
"public, max-age=300")
.removeHeader("Pragma")
.build()
}
.build()
}
El código crea un OkHttpClient con una caché de 10 MB y anula Cache-Control a través de NetworkInterceptor. Si el servidor no devuelve Cache-Control o usa Expires, el interceptor agrega public, max-age=300 (5 minutos). El interceptor elimina el obsoleto encabezado Pragma (HTTP/1.0) para compatibilidad. El almacenamiento en caché en iOS funciona de manera similar a través de URLCache.shared con las configuraciones memoryCapacity y diskCapacity.
La directiva stale-while-revalidate permite mostrar al usuario una caché obsoleta mientras la aplicación obtiene datos nuevos en segundo plano. Esto proporciona un efecto de respuesta instantánea: el usuario ve el contenido de inmediato y, después de un segundo, se actualiza a la versión actual. Compatible con OkHttp a partir de la versión 3.10 y URLCache en iOS 14+. Ejemplo: Cache-Control: max-age=3600, stale-while-revalidate=300 — 1 hora de caché actualizada, luego 5 minutos de mostrar datos obsoletos con actualización en segundo plano.
Diferentes tipos de recursos requieren diferentes estrategias de almacenamiento en caché. Veamos las configuraciones óptimas para escenarios típicos en el desarrollo móvil. Para contenido estático con un hash en el nombre del archivo (bundle.abc123.js), puede establecer max-age hasta 1 año con immutable. Para listas de API que se actualizan raramente (directorios, categorías) — max-age de 5 minutos a 1 hora con stale-while-revalidate.
| Tipo de recurso | Cache-Control | Explicación |
|---|---|---|
| Activos estáticos versionados | public, max-age=31536000, immutable | 1 año, los archivos no cambian (hash en URL) |
| Activos estáticos no versionados | public, max-age=86400, must-revalidate | 1 día con revalidación forzada después |
| API: datos de referencia | public, max-age=600, stale-while-revalidate=60 | 10 minutos de caché + 1 minuto obsoleto |
| API: datos de usuario | private, max-age=60 | 1 minuto, solo para un usuario específico |
| API: datos sensibles | no-store | Prohibición completa de caché |
| Páginas HTML | no-cache, must-revalidate | Validación en cada solicitud, 304 si no ha cambiado |
Es importante recordar la seguridad: para las respuestas que contienen datos personales del usuario, siempre establezca private. Sin esta directiva, un proxy público (por ejemplo, corporativo) puede almacenar en caché la respuesta y entregarla a otro usuario. Para tokens de autenticación e información de pago, use no-store — ni siquiera una caché privada debe almacenar estos datos en disco.
Para verificar la corrección de Cache-Control, use el encabezado Age (cuántos segundos se ha almacenado la caché) y X-Cache (hit/miss en CDN). En el navegador — la pestaña Network, la columna Size muestra “from disk cache” o “304 Not Modified”. Si un recurso debería almacenarse en caché pero se carga cada vez, verifique si el servidor está agregando Cache-Control: no-cache o Pragma: no-cache junto con sus directivas.
Preguntas frecuentes
max-age se aplica a todas las cachés (incluidos los navegadores), s-maxage solo se aplica a las cachés compartidas (proxies, CDN). Si se especifica s-maxage, la CDN ignora max-age y usa s-maxage. Esto permite establecer diferentes tiempos de vida para el navegador y la CDN.
No, después de enviar una respuesta con max-age, el cliente no realizará una solicitud hasta que expire el temporizador. Para una invalidación inmediata de la caché, debe cambiar la URL del recurso (agregar una versión/hash) y enviar notificaciones push o mensajes WebSocket para un restablecimiento forzado.
La directiva immutable (RFC 8246) le indica al navegador que el recurso nunca cambiará en esta URL. El navegador ni siquiera intenta realizar una solicitud condicional al actualizar la página — usa la caché hasta que expire max-age. Funciona solo con archivos versionados.
Googlebot tiene en cuenta Cache-Control: un almacenamiento en caché prolongado acelera el rastreo repetido. noindex con caché rápido está bien. no-store puede ralentizar la indexación porque Googlebot cargará la página desde cero cada vez. Un max-age demasiado corto aumenta la carga del servidor durante el rastreo.
A través de helmet o middleware: res.set('Cache-Control', 'public, max-age=3600'). Para archivos estáticos, use express.static con el parámetro maxAge: express.static('public', {maxAge: '1y'}). Para rutas dinámicas — individualmente en cada manejador.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también