I made a change to an endpoint that updated a product's price and called revalidatePath('/productos'). It worked. The price updated. What I didn't see until two days later is that same route was also serving filters, categories, and a comparison view that depended on the same layout — and all of that got regenerated from scratch on the next request, even though none of it had changed. Cache thrown in the trash from over-invalidating.
That's when I understood it wasn't a "which function do I use" problem — I was thinking in routes when I needed to be thinking in data. revalidatePath invalidates by where content lives. revalidateTag invalidates by what the content is. These are two different granularities, and picking the wrong one doesn't break anything visually — it breaks your cache silently, in a way you only notice by staring at build metrics or response times.
My take is simple: revalidatePath is the brute-force tool and revalidateTag is the precision one, and most people default to the first because it's the one that shows up first in the docs examples, not because it's the right call for their case. That's the actual thesis here, not just "these are two functions, use the right one" — the real cost is invisible until you go looking for it in your infra bill or your response times.
nextjs revalidate cache: what the official docs actually say
The official revalidateTag documentation is clear on a point a lot of people skip past: the function invalidates cache entries tied to a specific tag, no matter which route they're on. That means if three different pages use fetch(url, { next: { tags: ['productos'] } }), invalidating 'productos' hits all three — without touching anything else on those pages that doesn't depend on that tag.
What the docs don't say is when you should use tags instead of paths. That's left as an architecture decision, and it's exactly where most people wing it.
// revalidatePath: invalidates EVERYTHING generated under that route
import { revalidatePath } from 'next/cache'
export async function actualizarPrecio(id, precio) {
await db.productos.update(id, { precio })
revalidatePath('/productos') // brute force: recomputes the whole route
}
// revalidateTag: invalidates only what declared that tag
import { revalidateTag } from 'next/cache'
export async function actualizarPrecioTag(id, precio) {
await db.productos.update(id, { precio })
revalidateTag(`producto-${id}`) // precision: only what uses that tag
}
The difference isn't syntax. It's mental model: revalidatePath thinks in URLs, revalidateTag thinks in data dependencies. If your app has a 1-to-1 relationship between route and data, you won't notice the difference. If you have composition — a layout pulling data from multiple sources, or a component that repeats across multiple routes — that's where a bad choice starts costing you.
Where people screw this up (and the hidden cost)
The common playbook goes: "I changed a piece of data, I invalidate the route where it shows." That works fine in a tutorial, where the route usually maps to exactly one query — but production routes rarely stay that simple once a layout starts pulling from more than one source. The problem shows up the moment that same route serves content that doesn't depend on what you just changed.
Classic counterexample: a product detail page that also shows "related products" pulled from a different source. If you call revalidatePath('/productos/[id]') every time stock updates, you're also forcing the related-products section to regenerate, even though it didn't change. That extra work isn't free: every regeneration means re-running fetches, re-rendering server components, and rebuilding the cached HTML. Multiply that by traffic and it's pure server cost with zero benefit.
The inverse mistake also exists: using revalidateTag so granularly that you forget a tag on some fetch, and that piece stays stale indefinitely because nobody ever invalidates it. That's the classic "stale cache" — the correct data exists in the database, but the page keeps showing the old one because the tag never fired.
Both errors are symmetric: over-invalidation with revalidatePath burns extra compute; under-tagging with revalidateTag leaves stale data that nobody notices until a user reports "this is wrong." What's uncomfortable is that neither mistake throws an error — you find out via a metrics dashboard or an angry support ticket, months later.
This connects to something I already touched on when talking about Server Actions and TanStack Query: there the topic was the mutation itself, how you fire the change. Here the topic is different — what happens to the cache after the mutation already happened. Two separate layers, and mixing them up is part of the problem.
Decision matrix: when to use each
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 before you choose:
-
Use
revalidatePathwhen the entire route depends on the same data and there's no composition from different sources. Simple pages, blogs with a single query per post, landing pages. -
Use
revalidateTagwhen the same data shows up across multiple routes, or a route combines data from independent sources that change at different times. -
Avoid
revalidatePathon shared layouts — invalidating the root layout from one specific action is the most common form of over-invalidation, because it drags down everything rendered below it. -
Avoid
revalidateTagwithout a naming convention — if tags don't follow a clear pattern (producto-${id},usuario-${id}), it's easy to forget to tag a new fetch and generate silent stale cache. - Look first at which fetches share data before deciding. If you don't know which components read which source, the decision is going to be a guess, not architecture.
The limits of this guide
This is design judgment, not measurement. I don't have my own benchmarks on how much extra compute a badly-placed revalidatePath generates versus a well-placed revalidateTag — those numbers depend on component tree size, fetch count, and the infrastructure it runs on, and would vary case by case. What is verifiable is the documented behavior: revalidateTag invalidates by tag association, not by route, according to the official docs.
This also doesn't replace observability tooling. If you suspect over-invalidation on a real project, the way to confirm it is to log how many times each segment regenerates and compare that against actual data changes — not guess by reading the code. If you don't have that instrumentation yet, that's the first thing to build before touching your revalidation strategy.
Final take
If your app has more than three routes sharing some kind of data, start with revalidateTag from day one. Migrating from revalidatePath to tags after the project has already grown is more work than designing it right from the start, because it means auditing every fetch and retroactively adding tags without breaking anything that already worked.
revalidatePath isn't wrong — it's the right tool for the simple case. The mistake isn't using it, it's using it by default without asking yourself if your route is hiding more than one piece of data underneath. Next time you write a Server Action that mutates something, before reflexively throwing a revalidatePath, ask yourself: is this route one piece of data, or several taped together?
FAQ
Can revalidatePath and revalidateTag be combined in the same action?
Yes. They're not mutually exclusive. You can invalidate a specific tag and also the route if there's content that depends exclusively on that page and isn't tagged.
Does revalidateTag work with native fetch, or do I need a specific cache library?
It works with Next.js's extended fetch, using the next: { tags: [...] } option in the call. Outside of that mechanism (for example, direct database queries), you need to wrap the read in unstable_cache with your own tags for revalidateTag to have any effect.
Why does my page still show old data after calling revalidateTag?
The most common reason is that the fetch reading that data never declared that tag. Check that the tag string is exactly the same in the read and in the invalidation call — a typo there is invisible at a glance.
Does revalidatePath affect client cache or only server cache?
It affects the data cache and the server's Full Route Cache. The client's router cache (navigation between already-visited pages) gets invalidated separately and depends on how the navigation behavior is configured.
Should I use revalidateTag on every fetch just in case?
Not necessarily. Tagging everything without criteria adds maintenance complexity — every tag is a convention someone has to remember to keep updated. Use it where there's real data reuse across routes, not as a blanket habit.
Does this change with Server Actions compared to Route Handlers?
The behavior of revalidatePath and revalidateTag is the same in both contexts — the difference is where you call them from, not what they invalidate. If you're coming from mutating data with Server Actions and TanStack Query, the cache invalidation logic gets added after the mutation, it doesn't replace that layer.
Original source:
- Next.js Docs — revalidateTag: https://nextjs.org/docs/app/api-reference/functions/revalidateTag
This article was originally published on juanchi.dev
Top comments (0)