La caché HTTP es la palanca de rendimiento más barata de un sitio web: bien ajustada, evita peticiones enteras y sirve archivos desde un punto cercano al usuario. Mal ajustada, entrega contenido caducado o, al contrario, no cachea nada. Tres mecanismos cubren casi todas las necesidades: la cabecera Cache-Control, la revalidación mediante validadores y el versionado de las URL. Esta guía los enlaza en una estrategia coherente, del navegador al CDN.
Dos familias de caché, una sola cabecera pivote
Una caché HTTP entra en una de dos categorías. La caché privada es la del navegador, propia de un usuario. La caché compartida es la que atraviesan todos los visitantes: un CDN, un proxy inverso como Varnish o la caché de un servidor nginx. La distinción es decisiva, porque una respuesta personalizada nunca debe acabar en una caché compartida, donde se serviría de nuevo a otro visitante.
La cabecera que gobierna todo esto es Cache-Control, presente en la respuesta del servidor. Sustituye a los antiguos Expires y Pragma, que ya no hace falta emitir. Se lee directiva a directiva, y unas pocas bastan para describir la mayoría de las políticas. En un servidor autoalojado, estos ajustes se ponen de una vez por todas en la configuración del propio servidor web.
Las directivas Cache-Control que importan
| Directiva | Efecto | Uso típico |
|---|---|---|
max-age=N | Respuesta fresca durante N segundos | Duración por defecto |
s-maxage=N | Como max-age, pero solo para cachés compartidas | CDN más agresivo que el navegador |
public | Respuesta almacenable por una caché compartida | Archivos estáticos |
private | Almacenamiento reservado al navegador | Página personalizada |
no-cache | Almacenable, pero revalidada antes de cada uso | HTML que cambia a menudo |
no-store | Prohibición total de almacenar | Datos sensibles |
must-revalidate | Una vez caducada, la respuesta no se sirve sin revalidación | Contenido de exactitud crítica |
immutable | Sin revalidación mientras es fresca, incluso al recargar | Archivos con URL versionada |
stale-while-revalidate=N | Sirve la copia caducada hasta N s mientras carga una nueva en segundo plano | Respuestas que toleran un ligero desfase |
La confusión más frecuente enfrenta no-cache y no-store. La primera permite el almacenamiento pero impone una revalidación cada vez; la segunda prohíbe todo almacenamiento. Para un panel que muestra datos confidenciales, solo no-store es adecuada.
La revalidación: ETag y Last-Modified
Cuando una respuesta cacheada caduca, la caché no necesita descargarla entera de nuevo: pregunta al servidor si ha cambiado. Ese diálogo se apoya en dos validadores. El servidor emite un ETag, huella opaca del contenido, o un Last-Modified, fecha de última modificación. En la ronda siguiente, la caché devuelve esos valores en If-None-Match o If-Modified-Since.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=0, must-revalidate
ETag: "9c1e-5f3a2b"
# Petición siguiente, una vez caducada la respuesta:
GET /api/perfil HTTP/1.1
If-None-Match: "9c1e-5f3a2b"
# El servidor responde sin cuerpo si nada ha cambiado:
HTTP/1.1 304 Not Modified
ETag: "9c1e-5f3a2b"La respuesta 304 Not Modified no transporta cuerpo alguno: solo circulan las cabeceras. En un recurso voluminoso, el ahorro de ancho de banda es notable, y la latencia percibida cae porque el navegador reutiliza su copia local. Un ETag se declina en forma fuerte o débil, esta última con el prefijo W/ cuando basta una equivalencia semántica, sin exigir una identidad byte a byte.
Una caché bien ajustada no se mide por el número de archivos que almacena, sino por el número de peticiones que nunca tuvo que atender.
Caché compartida, caché privada y la cabecera Vary
Una misma URL puede devolver variantes: una versión comprimida en gzip o en brotli, una traducción según el idioma. La cabecera Vary indica a la caché compartida qué cabeceras de petición distinguen esas variantes. Sin ella, un CDN corre el riesgo de servir la versión inglesa a un visitante hispanohablante, o contenido comprimido a un cliente que no sabe descomprimirlo.
# Archivos estáticos versionados: caché larga e inmutable
location ~* \.(?:css|js|woff2|png|jpg|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Documento HTML: siempre revalidado
location / {
add_header Cache-Control "no-cache";
add_header Vary "Accept-Encoding";
}La regla general cabe en una frase: caché larga e inmutable para los archivos cuyo nombre lleva una huella, revalidación sistemática para el HTML que los referencia. Este reparto separa con limpieza lo que cambia de lo que no cambia nunca. También cuenta para la seguridad, pues una respuesta autenticada mal tipada que se filtra en una caché compartida es una brecha, y un socle de endurecimiento mínimo debería incluir esa comprobación.
Invalidación: versionar la URL en lugar de purgar
El problema reputado difícil de la caché es la invalidación. Purgar un CDN en cada despliegue es lento y falible. La técnica robusta consiste en cambiar la URL del archivo cuando cambia su contenido, incrustando una huella: app.9f3c1a.js en lugar de app.js. El archivo pasa entonces a ser elegible para un max-age de un año y para immutable, ya que una versión nueva llevará un nombre distinto. Los empaquetadores modernos generan esas huellas automáticamente, lo que hace la estrategia casi gratuita de aplicar.
El único documento que no puede versionarse es el punto de entrada HTML, cuya URL es pública y estable. Por eso permanece en no-cache, revalidado en cada visita: pesa poco y referencia los archivos con huella. El efecto de esta política se lee directamente en las métricas de rendimiento que se siguen en la analítica.
Comprobar estos ajustes en producción lleva unos segundos: una petición curl -I sobre un archivo estático debe devolver un Cache-Control largo e inmutable, y la misma petición sobre el HTML un no-cache. En el lado del CDN, la proporción entre respuestas servidas desde la caché y respuestas transmitidas al origen, expuesta a menudo mediante una cabecera de diagnóstico como X-Cache, mide la eficacia real de la política mucho mejor que el número de archivos almacenados. Una tasa de aciertos que se estanca casi siempre delata una cabecera mal puesta más que una caché insuficiente.
Tres trampas se repiten en producción. Poner un max-age largo en un archivo HTML sin versionar congela el sitio para los visitantes hasta que caduque. Olvidar Vary: Accept-Encoding detrás de un CDN provoca respuestas ilegibles. Por último, dejar que una caché compartida almacene una respuesta marcada por error como private expone los datos de un usuario a otro: revisar estos tres puntos antes del lanzamiento evita la mayoría de los incidentes.
Lo que hay que retener
La estrategia se reduce a tres decisiones. Elegir la duración de frescura con max-age y s-maxage según la caché que se busca. Activar la revalidación por ETag para convertir las recargas en respuestas 304 sin cuerpo. Versionar los archivos estáticos para combinar una caché de un año con una invalidación inmediata. El HTML sigue revalidado, los archivos con huella quedan congelados y Vary protege las variantes. Esta combinación cubre la gran mayoría de los sitios sin configuración exótica.
En los sitios que he retomado, el fallo de rendimiento más común no era la ausencia de caché, sino una caché mal orientada: HTML congelado durante un día, archivos estáticos revalidados en cada página. Ahora aplico una regla única desde que un sitio entra en producción, antes incluso de optimizar el resto: HTML en no-cache, estáticos con huella en immutable. Es el ajuste que más rinde con el menor esfuerzo, y nunca he tenido motivo para lamentarlo. — Simon Janvier
Para profundizar: la referencia de MDN sobre la caché HTTP y la especificación RFC 9111.
