DEV Community

Juan Torchia
Juan Torchia Subscriber

Posted on Originally published at juanchi.dev

revalidatePath es fuerza bruta, revalidateTag es precisión

Hice un cambio en un endpoint que actualizaba el precio de un producto y llamé revalidatePath('/productos'). Funcionó. El precio se actualizó. Lo que no vi hasta dos días después es que esa misma ruta servía filtros, categorías y una vista de comparación que dependían del mismo layout — y todo eso se volvió a generar de cero en la siguiente request, aunque nada de eso había cambiado. Cache tirado a la basura por invalidar de más.

Ahí entendí que no era un tema de "cuál función usar" sino de que estaba pensando en rutas cuando tenía que pensar en datos. revalidatePath invalida por dónde vive el contenido. revalidateTag invalida por qué es el contenido. Son dos granularidades distintas y elegir mal una no rompe nada visualmente — rompe el cache silenciosamente, de una forma que solo se nota mirando métricas de build o tiempos de respuesta.

Mi tesis, después de pisar ese charco: revalidatePath es la herramienta de fuerza bruta y revalidateTag es la de precisión, y la mayoría usa la primera por default porque es la que aparece primero en los ejemplos de la documentación, no porque sea la correcta para su caso. Y lo digo con la cicatriz fresca: elegí mal una vez y me enteré tarde.

nextjs revalidate cache: qué dice la documentación oficial

La documentación oficial de revalidateTag es clara en un punto que mucha gente pasa por alto: la función invalida entradas de cache asociadas a un tag específico, sin importar en qué ruta estén. Eso significa que si tres páginas distintas usan fetch(url, { next: { tags: ['productos'] } }), invalidar 'productos' las afecta a las tres — sin tocar nada más de esas páginas que no dependa de ese tag.

Lo que la documentación no dice es cuándo conviene usar tags en vez de paths. Eso queda como decisión de arquitectura, y es justo el punto donde la mayoría improvisa.

// revalidatePath: invalida TODO lo que se generó bajo esa ruta
import { revalidatePath } from 'next/cache'

export async function actualizarPrecio(id, precio) {
  await db.productos.update(id, { precio })
  revalidatePath('/productos') // fuerza bruta: recalcula toda la ruta
}

// revalidateTag: invalida solo lo que declaró ese tag
import { revalidateTag } from 'next/cache'

export async function actualizarPrecioTag(id, precio) {
  await db.productos.update(id, { precio })
  revalidateTag(`producto-${id}`) // precisión: solo lo que usa ese tag
}
Enter fullscreen mode Exit fullscreen mode

La diferencia no es sintáctica. Es de modelo mental: revalidatePath piensa en URLs, revalidateTag piensa en dependencias de datos. Si tu aplicación tiene una relación 1 a 1 entre ruta y dato, no vas a notar la diferencia. Si tenés composición — un layout que junta datos de varias fuentes, o un componente que se repite en múltiples rutas — ahí es donde una elección mal hecha te sale cara.

Dónde se equivoca la gente (y el costo oculto)

La receta común que circula en foros y tutoriales es: "cambié un dato, invalido la ruta donde se muestra". Esa receta funciona bien cuando el ejemplo es simple — una ruta, un dato, una relación directa — que es el caso típico de un tutorial. El problema aparece en proyectos reales cuando esa misma ruta sirve contenido que no depende del dato que cambiaste, algo que un ejemplo didáctico casi nunca modela.

Contraejemplo típico: una página de detalle de producto que también muestra "productos relacionados" con datos que vienen de otra fuente. Si usás revalidatePath('/productos/[id]') cada vez que se actualiza el stock, también estás forzando la regeneración de la sección de relacionados, que no cambió. Ese trabajo extra no es gratis: cada regeneración implica volver a ejecutar fetches, renderizar componentes de servidor y reconstruir el HTML cacheado. Multiplicado por tráfico, es costo de servidor sin beneficio.

El caso inverso también existe: usar revalidateTag de forma tan granular que te olvidás de un tag en algún fetch, y esa parte queda con datos viejos indefinidamente porque nadie la invalida. Ese es el "cache stale" clásico — el dato correcto existe en la base, pero la página sigue mostrando el anterior porque el tag nunca se disparó.

Los dos errores son simétricos: over-invalidation con revalidatePath gasta cómputo de más; under-tagging con revalidateTag deja datos viejos sin que nadie lo note hasta que un usuario reporta "esto está mal". Lo incómodo es que ambos fallan en silencio — no hay error en consola, no hay warning. Se nota en métricas o se nota cuando alguien se queja.

Esto conecta con algo que ya toqué al hablar de Server Actions y TanStack Query: ahí el tema era la mutación en sí, cómo disparar el cambio. Acá el tema es distinto — qué pasa con el cache después de que la mutación ya ocurrió. Son dos capas separadas y confundirlas es parte del problema.

Matriz de decisión: cuándo usar cada uno

flowchart TD
  A[Necesito invalidar cache] --> B{¿El dato tiene un tag propio declarado?}
  B -->|No, es 1 a 1 con la ruta| C[revalidatePath]
  B -->|Sí, se comparte entre rutas| D[revalidateTag]
  D --> E{¿El tag cubre todos los fetches relevantes?}
  E -->|No| F[Riesgo de cache stale: agregar tag faltante]
  E -->|Sí| G[Invalidación precisa, sin over-fetch]
  C --> H{¿La ruta tiene contenido no relacionado al cambio?}
  H -->|Sí| I[Riesgo de over-invalidation: separar por tag]
  H -->|No| J[Fuerza bruta aceptable]

Checklist antes de elegir:

  • Usá revalidatePath cuando la ruta entera depende del mismo dato y no hay composición de fuentes distintas. Páginas simples, blogs con una sola query por post, landing pages.
  • Usá revalidateTag cuando el mismo dato aparece en múltiples rutas, o una ruta combina datos de fuentes independientes que cambian en momentos distintos.
  • Evitá revalidatePath en layouts compartidos — invalidar el layout root desde una acción específica es la forma más común de over-invalidation, porque arrastra todo lo que renderiza debajo.
  • Evitá revalidateTag sin convención de nombres — si los tags no siguen un patrón claro (producto-${id}, usuario-${id}), es fácil olvidarse de etiquetar un fetch nuevo y generar cache stale silencioso.
  • Mirá primero qué fetches comparten datos antes de decidir. Si no sabés qué componentes leen qué fuente, la decisión va a ser adivinanza, no arquitectura.

Los límites de esta guía

Esto es criterio de diseño, no medición. No tengo benchmarks propios de cuánto cómputo extra genera un revalidatePath mal puesto contra un revalidateTag bien puesto — esos números dependen del tamaño del árbol de componentes, la cantidad de fetches y la infraestructura donde corre, y variarían caso a caso. Lo que sí es verificable es el comportamiento documentado: revalidateTag invalida por asociación de tag, no por ruta, según la documentación oficial.

Tampoco esto reemplaza herramientas de observabilidad. Si sospechás over-invalidation en un proyecto real, la forma de confirmarlo es loguear cuántas veces se regenera cada segmento y comparar contra los cambios de datos reales — no adivinar leyendo el código.

Postura final

Si tu app tiene más de tres rutas que comparten algún tipo de dato, arrancá con revalidateTag desde el día uno. Migrar de revalidatePath a tags después de que el proyecto creció es más trabajo que diseñarlo bien desde el principio, porque implica auditar cada fetch y agregar tags retroactivamente sin romper nada que ya funcionaba.

revalidatePath no es incorrecto — es la herramienta correcta para el caso simple. El error no está en usarlo, está en usarlo por default sin preguntarte si tu ruta esconde más de un dato debajo. Lo que no te perdono a esta altura es seguir tirando revalidatePath en un layout compartido "porque siempre funcionó así". La próxima vez que escribas una Server Action que mute algo, antes de tirar un revalidatePath reflejo, preguntate: ¿esta ruta es un dato o son varios pegados con cinta?

Preguntas frecuentes

¿revalidatePath y revalidateTag se pueden combinar en la misma acción?
Sí. No son mutuamente excluyentes. Podés invalidar un tag específico y además la ruta si hay contenido que depende exclusivamente de esa página y no está tageado.

¿revalidateTag funciona con fetch nativo o necesito una librería de cache específica?
Funciona con el fetch extendido de Next.js, usando la opción next: { tags: [...] } en la llamada. Fuera de ese mecanismo (por ejemplo, consultas directas a una base de datos), necesitás envolver la lectura en unstable_cache con tags propios para que revalidateTag tenga efecto.

¿Por qué mi página sigue mostrando datos viejos después de llamar revalidateTag?
El motivo más común es que el fetch que lee ese dato nunca declaró ese tag. Revisá que el string del tag sea exactamente igual en la lectura y en la invalidación — un typo ahí es indetectable a simple vista.

¿revalidatePath afecta el cache del cliente o solo el del servidor?
Afecta el cache de datos y el Full Route Cache del servidor. El router cache del cliente (la navegación entre páginas ya visitadas) se invalida por separado y depende de cómo esté configurado el comportamiento de navegación.

¿Conviene usar revalidateTag en todos los fetches por las dudas?
No necesariamente. Tagear todo sin criterio agrega complejidad de mantenimiento — cada tag es una convención que alguien tiene que recordar actualizar. Usalo donde hay reutilización real de datos entre rutas, no como hábito generalizado.

¿Esto cambia con Server Actions comparado con Route Handlers?
El comportamiento de revalidatePath y revalidateTag es el mismo en ambos contextos — la diferencia está en desde dónde los llamás, no en qué invalidan. Si venís de mutar datos con Server Actions y TanStack Query, la lógica de invalidación de cache se agrega después de la mutación, no reemplaza esa capa.


Fuente original:


Este artículo fue publicado originalmente en juanchi.dev

Top comments (0)