
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
/pageor/layoutto the string yourself — that's what thetypeparameter is for. - It's case-sensitive.
- It has a hard 1024-character limit.
- You don't need a trailing slash regardless of your
trailingSlashconfig 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:
| Function | What it invalidates | Scope |
|---|---|---|
revalidatePath | A specific page or layout path | Just that path (and nested paths under an invalidated layout) |
revalidateTag | Data marked stale by tag | Every page that fetches data with that tag |
updateTag | Data expired by tag | Every 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
| Question | Answer |
|---|---|
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 |


