Type something to search...
Next.js revalidatePath

Next.js revalidatePath

Every caching system eventually needs an escape hatch — a way to say "forget what you know about this one thing, go get it fresh." In Next.js, revalidatePath is one of two escape hatches for exactly that (the other being revalidateTag/updateTag), and it's the one that thinks in terms of routes rather than data. You give it a path, and Next.js throws away whatever it had cached for that path so the next visitor gets a rebuilt version instead of a stale one.

That framing — "routes, not data" — is the whole key to using revalidatePath correctly. It's easy to reach for it as a generic "clear the cache" button, and it mostly works that way in simple apps, but the moment your data is shared across more than one route, that mental model breaks down in a way that produces genuinely confusing bugs. This article covers the full API surface, then spends real time on the part the docs only gesture at: what revalidatePath does not invalidate, and why that trips people up constantly.

Where you can call it

revalidatePath only runs in server environments — specifically Server Functions (Server Actions) and Route Handlers. It cannot be called from Client Components, and it cannot be called from Proxy (the file that replaced Middleware). That last restriction surprises people who reach for Proxy as a general-purpose "run code before every request" hook — it isn't a place to trigger cache invalidation, because Proxy runs in the Edge Runtime and doesn't have access to the server-side cache store the way a Server Function or Route Handler does.

The two contexts where it is valid behave slightly differently, and this is easy to miss on a first read:

In a Server Function, calling revalidatePath updates the UI immediately if the user is currently viewing the affected path. It currently also refreshes previously-visited pages the next time you navigate back to them — a side effect the docs flag as temporary, expected to narrow to just the specified path in a future release.

In a Route Handler, there's no "current page" to update immediately, so revalidatePath just marks the path stale. The actual refetch happens lazily, on the next request to that path. If you call it with a dynamic segment pattern, that doesn't fire off a wave of revalidations for every matching page right now — it just flags the pattern, and each matching page gets its fresh data whenever it's next requested.

The signature

revalidatePath(path: string, type?: 'page' | 'layout'): void

path is either a literal path (/product/123) or a route pattern with a dynamic segment (/product/[slug]). A few rules that aren't obvious from the type signature alone:

  • Don't append /page or /layout to the string yourself — that's what the type parameter is for.
  • It's case-sensitive.
  • It has a hard 1024-character limit.
  • You don't need a trailing slash regardless of your trailingSlash config setting.

type is optional, but only when path is a literal path. The moment path contains a dynamic segment like [slug], type becomes required, because Next.js needs to know whether you mean "the page file at this pattern" or "the layout file at this pattern" — the pattern alone is ambiguous between the two.

The function returns nothing. There's no promise to await, no confirmation object — it fires and forgets, which is part of why it's easy to call it and assume it did more than it did.

What actually gets invalidated

path can point at three different kinds of things, and each behaves distinctly:

Pages — a literal path like /blog/post-1 invalidates just that one rendered page.

Layouts — using type: 'layout' invalidates the layout.tsx at that segment, every nested layout beneath it, and every page beneath those. This is the one with real blast radius. revalidatePath('/blog/[slug]', 'layout') doesn't just refresh the blog post layout — it cascades down to every page that layout wraps.

Route Handlers — you can target a Route Handler's own cached data directly:

// app/api/data/route.ts
export async function GET() {
  const data = await fetch("https://api.vercel.app/blog", {
    cache: "force-cache",
  });

  return Response.json(await data.json());
}

Calling revalidatePath('/api/data') invalidates the cached response from that fetch call, the same as it would for a page.

There's also a scorched-earth option: revalidatePath('/', 'layout') invalidates the root layout, which cascades to every page in the app and purges the client-side Router Cache on top of it. This is the closest thing to a global cache-bust button Next.js gives you, and it should be treated with the same caution as any operation with app-wide blast radius — it's a legitimate tool for "we just deployed a content migration and need everything fresh," not something to wire into a routine user action.

The rewrite gotcha

If your app uses rewrites in next.config.js, there's a subtlety that will cost you a confused debugging session if you don't know about it up front. revalidatePath operates on the route file structure, not the URL the browser shows.

Say you have this rewrite:

// next.config.js
module.exports = {
  async rewrites() {
    return [{ source: "/blog", destination: "/news" }];
  },
};

Visitors see /blog in their address bar, but the actual route file lives at app/news/page.tsx. Cache entries are tagged by the route file that produced them — so you have to revalidate the destination, not the source:

// Correct
revalidatePath("/news");

// Silently does nothing useful
revalidatePath("/blog");

The second call doesn't error. It just doesn't match anything, and you're left wondering why your "fresh" content still shows the old version. If your app has any rewrites at all, this is the first thing to check when a revalidatePath call seems to have no effect.

Why your other pages didn't update

This is the part worth reading twice. revalidatePath, revalidateTag, and updateTag solve overlapping but distinct problems:

FunctionWhat it invalidatesScope
revalidatePathA specific page or layout pathJust that path (and nested paths under an invalidated layout)
revalidateTagData marked stale by tagEvery page that fetches data with that tag
updateTagData expired by tagEvery page that fetches data with that tag

The failure mode this produces: imagine two pages both fetching the same tagged data.

// Page A: /blog
const posts = await fetch("https://api.vercel.app/blog", {
  next: { tags: ["posts"] },
});

// Page B: /dashboard
const recentPosts = await fetch("https://api.vercel.app/blog?limit=5", {
  next: { tags: ["posts"] },
});

Call revalidatePath('/blog'), and only /blog gets fresh data on the next visit. /dashboard keeps serving its cached version, because revalidatePath never touched the posts tag — it invalidated the page, not the underlying tagged fetch. If your data model has any sharing across routes (and most real apps do — a "recent posts" widget in a sidebar, a related-content block, anything reused), path-only revalidation quietly leaves those other surfaces stale.

The fix the docs recommend, and the one worth adopting as a default pattern, is to call both together in a shared utility whenever a mutation could affect data used elsewhere:

"use server";

import { revalidatePath, updateTag } from "next/cache";

export async function updatePost() {
  await updatePostInDatabase();

  revalidatePath("/blog"); // Refresh the blog page itself
  updateTag("posts"); // Refresh every page using the 'posts' tag
}

Treat revalidatePath as "make sure this specific page is current" and revalidateTag/updateTag as "make sure this specific data is current everywhere it appears." A mutation that changes shared data almost always needs both, not one or the other.

Worked examples

A specific path:

import { revalidatePath } from "next/cache";
revalidatePath("/blog/post-1");

A page pattern (invalidates every page matching the pattern, but not nested routes beneath it):

import { revalidatePath } from "next/cache";
revalidatePath("/blog/[slug]", "page");
// with a route group
revalidatePath("/(main)/blog/[slug]", "page");

/blog/[slug] won't cascade to invalidate /blog/[slug]/[author] — that's a separate page file with its own cache entry.

A layout pattern (this one does cascade):

import { revalidatePath } from "next/cache";
revalidatePath("/blog/[slug]", "layout");

Here, everything nested beneath that layout — including /blog/[slug]/[another] — gets invalidated along with it.

Inside a Server Function, the common shape:

// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";

export default async function submit() {
  await submitForm();
  revalidatePath("/");
}

Inside a Route Handler, typically as an on-demand revalidation endpoint hit by a webhook:

// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import type { NextRequest } from "next/server";

export async function GET(request: NextRequest) {
  const path = request.nextUrl.searchParams.get("path");

  if (path) {
    revalidatePath(path);
    return Response.json({ revalidated: true, now: Date.now() });
  }

  return Response.json({
    revalidated: false,
    now: Date.now(),
    message: "Missing path to revalidate",
  });
}

A practical note the docs don't cover: lock this endpoint down

That last example is copied almost verbatim from the official docs, and it's worth flagging as unsafe to ship as-is. It's a public GET request that revalidates whatever path a caller asks for, with no authentication at all. If this route is reachable in production, anyone who discovers it can trigger cache invalidation on demand across your entire site — which, at the very least, is a cheap way to hammer your origin with regenerations, and depending on your caching setup could be used as a crude denial-of-service lever.

At minimum, gate it behind a shared secret:

export async function GET(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get("secret");
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ message: "Invalid token" }, { status: 401 });
  }

  const path = request.nextUrl.searchParams.get("path");
  if (path) {
    revalidatePath(path);
    return Response.json({ revalidated: true, now: Date.now() });
  }

  return Response.json({ revalidated: false, now: Date.now() });
}

Most CMS webhook systems (Sanity, Contentful, WordPress plugins that ping a revalidation URL) support sending a static secret as a query param or header — wire it up on both ends before this route ever sees production traffic.

Common mistakes

Forgetting type for a dynamic segment. revalidatePath('/blog/[slug]') without a second argument doesn't throw, but it also doesn't do what most people expect — you need to be explicit about 'page' or 'layout' the moment the path contains a bracketed segment.

Assuming it invalidates shared data. Covered above, but common enough to repeat: if two routes fetch the same tagged data, revalidatePath on one of them doesn't touch the other.

Revalidating the source path instead of the destination when rewrites are involved — silent no-op, not an error.

Treating revalidatePath('/', 'layout') as routine. It's the right tool for "we need everything fresh right now," not for "a single blog post was edited." Reach for a scoped path first.

Calling it from a Client Component or Proxy. Both fail because neither runs in a context with access to the server cache — this one usually surfaces as an immediate runtime error rather than a silent failure, which at least makes it easy to catch.

Key Takeaways

QuestionAnswer
Where can I call revalidatePath?Server Functions and Route Handlers only — not Client Components, not Proxy
What's the difference between path alone and path + type?type is required once path contains a dynamic segment; it disambiguates page vs. layout
Does invalidating a layout affect nested pages?Yes — layout invalidation cascades to every nested layout and page beneath it
Does revalidatePath refresh other pages sharing the same tagged data?No — use revalidateTag/updateTag for that, often alongside revalidatePath
What if my route uses a next.config.js rewrite?Revalidate the destination path (the real route file), not the source path users see
Is revalidatePath('/', 'layout') safe to call often?It's expensive — full-app invalidation plus a client cache purge. Use scoped paths where possible
Should a public revalidation Route Handler be left unauthenticated?No — gate it with a shared secret before it's exposed in production
Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper