Saltar al contenido

Next.js revalidateTag: invalida solo lo que cambia en tu caché ISR

El revalidate por tiempo es un suelo, no una estrategia: con etiquetas y revalidación bajo demanda actualizas solo lo que cambió. Flujo, código de referencia y trampas.

PorSergio Jáuregui

8 min de lectura

Portada sobre Next.js revalidateTag: diagrama de nodos cian donde un solo nodo rojo envía un pulso a una página y el resto sigue apagado.

Next.js revalidateTag invalida las entradas del caché ISR marcadas con una etiqueta, sin esperar a que expire el revalidate por tiempo. Es la pieza que permite actualizar una página en cuanto cambia el dato, en lugar de confiar en un intervalo. El revalidate por tiempo queda como red de seguridad, no como estrategia.

Esta guía explica el patrón completo: etiquetar los fetch, proteger el endpoint de revalidación, disparar la invalidación desde el backend y comprobar que funciona. Los fragmentos de código son una implementación de referencia pensada para Next.js 14 con App Router; en versiones más recientes conviene revisar la firma de revalidateTag en la documentación.

Por qué el revalidate por tiempo no basta

El revalidate por tiempo regenera la entrada cuando caduca: entre una edición y la caducidad, la página sirve el HTML anterior. Bajar el intervalo refresca antes, pero golpea más al backend; subirlo alivia al backend, pero alarga lo obsoleto. Es un suelo, no una estrategia. La estrategia es invalidar justo lo que cambió, en el momento en que cambia.

Un valor por defecto razonable es un revalidate moderado para casi todo, y no-store solo para lo que nunca debe cachearse, como un borrador o una vista previa. Sobre esa base, las etiquetas se encargan del resto.

Cómo etiquetar cada fetch para Next.js revalidateTag

Etiquetar es asociar cada fetch a uno o varios nombres para invalidarlo después por nombre. La documentación de revalidateTag describe esa semántica: llamas a la función con una etiqueta y las entradas marcadas con ella se invalidan bajo demanda.

Un patrón útil combina una etiqueta gruesa, para el conjunto, y una fina, para el elemento concreto:

const res = await fetch(`${API_URL}/api/products/${encodeURIComponent(slug)}`, {
  next: { revalidate: 300, tags: ['products', `product:${slug}`] },
});

La etiqueta gruesa cubre listados y páginas relacionadas; la fina, la ficha concreta. Invalidar de más tiene un coste razonable: una regeneración extra. Invalidar de menos deja contenido viejo visible, así que la regla es etiquetar con generosidad y no omitir ningún fetch que muestre datos editables. Lo efímero no se etiqueta: se marca no-store.

Un detalle que pasa desapercibido: un fetch sin etiqueta se cachea por tiempo, pero la revalidación bajo demanda no puede alcanzarlo. Si el dato debe refrescarse al editarlo, la etiqueta va desde el primer día.

El endpoint de revalidación se protege con un secreto

Cada frontend expone un POST que recibe etiquetas y las invalida. Sin protección, cualquiera podría vaciar tu caché a voluntad y dirigir tráfico contra el backend. Por eso el endpoint exige una cabecera con un secreto compartido solo entre backend y frontend, y devuelve 401 si falta o no coincide. Si el secreto no está configurado, el endpoint queda deshabilitado.

import { revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const expected = process.env.REVALIDATE_SECRET;
  const provided = request.headers.get('x-revalidate-secret');

  if (!expected || provided !== expected) {
    return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json().catch(() => ({}));
  const tags: string[] =
    Array.isArray(body?.tags) && body.tags.length > 0 ? body.tags : ['products'];

  tags.forEach((tag) => revalidateTag(tag));
  return NextResponse.json({ success: true, revalidated: tags });
}

Aceptar una lista de etiquetas evita atar el endpoint a una sola. La respuesta devuelve las etiquetas invalidadas, que es lo primero que conviene mirar al probarlo a mano. Nunca aceptes invalidaciones por un GET abierto ni expongas el secreto en el cliente.

Disparar la revalidación desde el backend sin esperar

Tras una mutación que cambie lo que se muestra, el backend avisa al frontend. El disparo debe ser fire-and-forget: no espera la respuesta, nunca lanza y no hace nada si falta configuración. Así la respuesta al administrador no depende de que el frontend esté disponible en ese instante.

export function revalidateFrontend(url: string | undefined, tags: string[]): void {
  const secret = process.env.REVALIDATE_SECRET;
  if (!url || !secret) return;

  fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'x-revalidate-secret': secret },
    body: JSON.stringify({ tags }),
  })
    .then((res) => {
      if (!res.ok) console.warn('Revalidate request failed', res.status);
    })
    .catch((err) => console.warn('Revalidate request error', err));
}

// En cada mutación:
await prisma.product.update({ where: { id }, data });
revalidateFrontend(process.env.FRONTEND_REVALIDATE_URL, ['products']);

Un complemento útil es mantener en el backend una versión de caché (un contador o una marca de tiempo) que cambie en cada mutación y viaje junto a las etiquetas. Sirve para depurar: si la versión no cambió, la mutación no pasó por el disparo.

El punto que más se olvida es la cobertura. Crear, editar, borrar y publicar son casos obvios. Las importaciones masivas, los cambios en tablas auxiliares como las categorías y las redirecciones suelen quedarse fuera. Antes de dar el patrón por terminado, recorre todas las rutas de escritura y pregunta si cambian algo que el frontend muestra.

Las trampas que conviene conocer

La primera es el no-op silencioso. Sin variables de entorno, o con el frontend caído en el momento del disparo, la edición se guarda pero la página sirve HTML viejo hasta que caduca el intervalo. La API responde con éxito igual, así que solo el log avisa. Por eso el aviso debe quedar registrado y revisarse.

La segunda es la etiqueta ausente. Un fetch que no declara la etiqueta que la ruta invalida nunca se refresca por esa vía, y no da ningún error. Revisa que cada etiqueta invalidada exista en algún fetch, y al revés.

La tercera es la instancia única frente a varias. La caché de datos de Next vive en cada instancia del servidor. Con un solo contenedor, un POST basta. Si escalas a varias réplicas tras un balanceador, un POST alcanza a una sola y las demás siguen sirviendo lo viejo hasta que caducan. Ahí hace falta enviar la invalidación a todas las instancias o aceptar el intervalo como límite.

La cuarta es el orden de despliegue. Si el backend empieza a disparar invalidaciones hacia una ruta de frontend que aún no existe, los POST fallan en silencio. Despliega primero el frontend con la ruta de revalidación y después el backend que la llama.

Cómo comprobar que la revalidación funciona

Hazlo en dos pasos. Primero el endpoint: un POST sin cabecera debe devolver 401, y con el secreto correcto debe devolver la lista de etiquetas invalidadas. Eso confirma que la ruta existe, que el secreto coincide y que revalidateTag se ejecutó.

Después, el flujo completo: edita el dato, revisa el aviso o la versión en el log y recarga la página antes de que caduque el intervalo. Si el cambio ya se ve, el patrón funciona; si aparece solo al caducar el intervalo, algo falló en silencio. La prueba es de comportamiento, antes o después del intervalo, y no requiere herramientas especiales.

Cuándo no usar revalidación bajo demanda

Los borradores y las vistas previas no deben cachearse, así que no hay nada que invalidar: no-store es la respuesta. Las páginas que cambian en cada visita tampoco se benefician, porque cachear un dato que no se reutiliza solo añade invalidaciones inútiles.

Si no puedes proteger el endpoint, no lo montes: un POST abierto invita a dirigir tu tráfico contra tu propio backend. Y si el contenido cambia poco y un intervalo corto ya parece fresco, el revalidate por tiempo a secas da menos piezas que mantener y menos fallos silenciosos.

Un criterio práctico: revalidación bajo demanda para contenido publicado que se edita a mano y se lee mucho; intervalo a secas para lo demás. Ambos conviven en el App Router, y mezclarlos por ruta es lo habitual.

Como referencia, el blog de sergioja.com sigue esta misma idea: sus fetch están etiquetados y un endpoint protegido recibe las invalidaciones desde el backend. Su arquitectura general está descrita en la implementación de referencia.

Etiqueta tus fetch y conecta el disparo hoy

Añade etiquetas a cada fetch que deba refrescarse al editar, crea la ruta con comprobación de secreto, llama a la invalidación desde cada mutación sin bloquear la respuesta y prueba el 401 y el flujo completo. Deja el intervalo como red: si algo falla en silencio, la página se corrige al caducar.

El caché es solo un frente de rendimiento. En la guía sobre miniaturas WebP con sharp verás cómo servir imágenes pequeñas con caché LRU en lugar de base64 gigante, y en la de hreflang en Next.js, la otra mitad menos visible: que Google entienda un sitio bilingüe. Si quieres este patrón en tu web, escríbeme desde la página de contacto.

¿Tienes un proyecto en mente?

Soy Sergio Jáuregui, desarrollador Full Stack. Si te ha gustado este artículo, mira mi trabajo o escríbeme y lo hablamos.

Seguir leyendo

Actualizado el 15 oct 2026

Volver arriba