DEV Community

Anas Sheikh
Anas Sheikh

Posted on

revalidatePath Might Not Be Invalidating What You Think It Is

If you've ever called revalidatePath after a mutation, watched the data still show up stale on the page, and fixed it by just also calling revalidatePath on a few more related paths until it finally worked, you were probably fighting the actual behavior of the function rather than a bug in your own code. This one has a genuinely confusing default that catches a lot of people, myself included the first time I ran into it.

The Setup That Looks Like It Should Just Work

// actions/posts.ts
'use server';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  await Post.create({ title: formData.get('title') });
  revalidatePath('/blog');
}
Enter fullscreen mode Exit fullscreen mode

A new post gets created, revalidatePath('/blog') runs, and yet the blog index sometimes still shows the old, stale list on the very next visit. Nothing throws an error. It just quietly doesn't update the way you'd expect, and the natural instinct is to assume the mutation itself failed, when the actual data was written correctly the whole time.

What's Actually Happening

revalidatePath takes an optional second argument, 'page' or 'layout', and it changes what actually gets invalidated:

revalidatePath('/blog');              // defaults to invalidating just this specific path
revalidatePath('/blog', 'page');      // same as above, explicit
revalidatePath('/blog', 'layout');    // invalidates this path AND all paths sharing its layout
Enter fullscreen mode Exit fullscreen mode

Without the second argument, revalidatePath('/blog') only invalidates that exact path. If your blog index is actually rendered through a layout that also affects nested routes, /blog/[slug], and you're expecting a change on the index to also refresh those nested pages, the default page-scoped call doesn't do that. You need the 'layout' variant explicitly if the invalidation is meant to cascade.

An Even More Common Gotcha: Dynamic Segments

// This does NOT invalidate /blog/my-first-post or any other specific slug
revalidatePath('/blog/[slug]');

// This DOES invalidate the specific rendered path
revalidatePath('/blog/my-first-post');
Enter fullscreen mode Exit fullscreen mode

Passing the literal route pattern, [slug] included as text, does not magically invalidate every dynamically rendered version of that route. revalidatePath needs the actual, real path that was rendered, not the file-system pattern that generated it. If you're revalidating a specific post after an edit, you need the specific slug, not the dynamic segment's placeholder syntax.

Why This Is Genuinely Confusing to Debug

The failure mode here is not an error, it's stale data that sometimes seems to resolve itself anyway, since a full page reload, a different route triggering revalidation elsewhere, or the cache eventually expiring on its own can mask the actual bug intermittently. This makes it feel inconsistent and hard to reproduce reliably, when the actual rule underneath it is completely deterministic, it's just not the rule most people assume by default.

When to Reach for revalidateTag Instead

For anything genuinely complex, several routes that share underlying data but don't share a clean path or layout relationship, revalidateTag is usually the more precise, more maintainable tool, tagging the actual data dependency rather than trying to map out every path relationship by hand.

// lib/queries/posts.ts
export const getPosts = unstable_cache(
  async () => Post.find().sort({ createdAt: -1 }).lean(),
  ['posts-list'],
  { tags: ['posts'] }
);

export const getPost = unstable_cache(
  async (slug: string) => Post.findOne({ slug }).lean(),
  ['post-detail'],
  { tags: ['posts'] }
);
Enter fullscreen mode Exit fullscreen mode
// actions/posts.ts
'use server';
export async function createPost(formData: FormData) {
  await Post.create({ title: formData.get('title') });
  revalidateTag('posts'); // invalidates every cached query tagged 'posts', regardless of path
}
Enter fullscreen mode Exit fullscreen mode

This sidesteps the entire path-matching question, since it invalidates based on what data was actually affected, not which specific URL path happens to render that data.

The Actual Checklist

If you need a change to cascade to nested routes under a shared layout, pass 'layout' explicitly, the default won't do this for you.

Never pass a route pattern with the literal [param] syntax expecting it to match every rendered instance. Pass the actual, specific path that was rendered.

For data shared across multiple, not-cleanly-related paths, reach for revalidateTag instead of trying to enumerate every affected path by hand with revalidatePath.

If a page still seems stale after revalidation, check whether the actual issue is a missing 'layout' argument or a route pattern being passed literally, before assuming the underlying mutation itself failed.


If you've hit stale data after a revalidatePath call and eventually "fixed" it by adding a few more calls until it worked, genuinely curious whether it turned out to be one of these two issues, or something else entirely. Drop what actually fixed it in the comments.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)