Evitar la caché del navegador: técnicas modernas con HTTP

Artículo actualizado en mayo de 2026. La versión original, publicada en el año 2000, cubría exclusivamente las respuestas de ASP clásico. Esta revisión aborda el mecanismo de caché HTTP en su conjunto y muestra cómo controlarlo desde PHP, Node.js, Apache y nginx.

Cómo funciona la caché del navegador

Cuando visitas una página por primera vez, el navegador descarga todos sus recursos: HTML, CSS, imágenes, scripts. La segunda vez, antes de volver a pedirlos al servidor, comprueba si los tiene guardados en local y si siguen siendo válidos. Ese mecanismo es la caché del navegador, y sirve para acelerar la navegación y reducir el ancho de banda consumido.

El problema aparece con las páginas dinámicas: si el servidor genera HTML diferente en cada petición (datos en tiempo real, paneles de administración, resultados personalizados), la caché puede mostrar una versión antigua sin que el usuario lo sepa. Evitarlo requiere enviar las cabeceras HTTP correctas.

Las cabeceras que controlan la caché

La cabecera principal es Cache-Control, definida en el RFC 7234. Sus directivas más importantes para desactivar la caché son:

Directiva

Efecto

no-store

El navegador no guarda nada. Cada visita descarga el recurso completo desde el servidor. Es la opción más agresiva.

no-cache

El navegador guarda el recurso pero siempre consulta al servidor antes de usarlo (mediante ETag o Last-Modified). Solo lo sirve desde caché si el servidor confirma que no ha cambiado.

must-revalidate

Si la copia en caché ha expirado, el navegador debe pedir confirmación al servidor antes de usarla, sin excepciones.

private

El recurso puede guardarse en el navegador del usuario pero no en proxies ni CDNs intermedios.

max-age=0

La copia en caché expira inmediatamente. Combinado con must-revalidate obliga a revalidar siempre.

La combinación habitual para páginas dinámicas que no deben cachearse en absoluto es:

Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Expires: 0

Pragma: no-cache y Expires: 0 son cabeceras heredadas de HTTP/1.0 que se incluyen por compatibilidad con proxies antiguos.

Implementación en PHP

<?php
// Evitar cualquier caché en páginas dinámicas sensibles
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Cache-Control: post-check=0, pre-check=0', false); // IE legacy
header('Pragma: no-cache');
header('Expires: 0');
?>

Estas cabeceras deben enviarse antes de cualquier salida HTML. Si usas un framework como Laravel o Symfony, ambos tienen middleware o helpers específicos para ello:

// Laravel: en el controlador o en una ruta
return response()->view('mi-vista')
    ->header('Cache-Control', 'no-store, no-cache, must-revalidate')
    ->header('Pragma', 'no-cache')
    ->header('Expires', '0');

Implementación en Node.js (Express)

// Middleware global para rutas dinámicas
app.use((req, res, next) => {
  res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
  res.set('Pragma', 'no-cache');
  res.set('Expires', '0');
  next();
});

// O en una ruta concreta
app.get('/dashboard', (req, res) => {
  res.set('Cache-Control', 'no-store');
  res.json({ data: obtenerDatosEnTiempoReal() });
});

Configuración en Apache (.htaccess)

<IfModule mod_headers.c>
    # Sin caché para páginas PHP dinámicas
    <FilesMatch ".(php)$">
        Header set Cache-Control "no-store, no-cache, must-revalidate"
        Header set Pragma "no-cache"
        Header set Expires "0"
    </FilesMatch>
</IfModule>

Configuración en nginx

location ~ .php$ {
    fastcgi_pass   unix:/run/php/php8.2-fpm.sock;
    fastcgi_index  index.php;
    include        fastcgi_params;

    # Sin caché
    add_header Cache-Control "no-store, no-cache, must-revalidate";
    add_header Pragma        "no-cache";
    add_header Expires       "0";
}

Caché inteligente con ETag: el enfoque recomendado para APIs

Para APIs o recursos que cambian con poca frecuencia, el enfoque más eficiente no es eliminar la caché sino validarla con ETags. El servidor asigna una huella digital al recurso; el navegador la guarda y la envía en la siguiente petición. Si la huella coincide, el servidor responde con un 304 Not Modified sin cuerpo, ahorrando ancho de banda. Si ha cambiado, devuelve el recurso actualizado con un nuevo ETag.

<?php
$datos     = obtenerDatos();
$etag      = md5(serialize($datos));
$etagCliente = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';

header('ETag: "' . $etag . '"');
header('Cache-Control: no-cache');  // valida siempre, pero puede usar caché si ETag coincide

if ($etagCliente === '"' . $etag . '"') {
    http_response_code(304);
    exit;
}

header('Content-Type: application/json');
echo json_encode($datos);

Resumen de estrategias

Caso de uso

Cabecera recomendada

Panel de admin, datos en tiempo real

Cache-Control: no-store

Página dinámica, datos que cambian poco

Cache-Control: no-cache + ETag

Página pública estática (HTML)

Cache-Control: max-age=3600

Assets con hash en el nombre (JS, CSS)

Cache-Control: max-age=31536000, immutable

Si usas PHP y buscas una solución de caché de aplicación más completa —para reducir consultas a la base de datos, no para controlar la caché del navegador—, el artículo sobre Phpfastcache explica cómo implementar caché de datos en capas con distintos backends.

Imagen: Pexels / Brett Sayles

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
ARTÍCULO ANTERIOR

SIGUIENTE ARTÍCULO