What this usually means
When revalidatePath fails to refresh cache, it’s almost always due to a mismatch between the request path you’re invalidating and the actual cached route, or a misunderstanding of how Next.js caching and fetch request deduplication work. Dynamic route parameters, trailing slash confusion, custom headers, or improper use of fetch cache directives can all cause cache to persist unexpectedly. Sometimes, the underlying mutation (like a DB write) succeeds, but the cache is never actually notified due to flawed revalidation logic.
The first ten minutes — establish facts before touching code.
- 1Check logs for revalidation events: tail -f .next/server/logs/*.log or instrument your API handler to log revalidatePath calls.
- 2Open DevTools → Network, reload the page, and inspect response headers for x-nextjs-cache: should be MISS post-invalidation.
- 3Visit the API route used for data fetch (e.g. /api/posts/[slug]) directly and compare its content to the stale page.
- 4Review the arguments passed to revalidatePath—ensure you pass the path, not a route pattern (e.g. /blog/my-post, not /blog/[slug]).
- 5Test revalidatePath via API or server action in isolation with a static path to rule out parameterization bugs.
- 6Search codebase for fetch requests with hardcoded cache: 'force-cache' or 'no-store', which override ISR behavior.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchapp/api/ or pages/api/ API route handlers triggering revalidatePath
- searchServer action files containing revalidatePath invocations
- searchnext.config.js rewrites or redirects that might affect routing
- search.next/server/app/pages.json for actual cached paths
- searchRelevant fetch() calls for caching directives (e.g., cache: 'force-cache')
- searchVercel dashboard (if hosted) → Deployments → Functions Logs
- searchProduction network traces for response header differences
Practical causes, not theory. These are the things you will actually find.
- warningCalling revalidatePath with a generic path (e.g., /blog/[slug]) instead of a fully resolved URL
- warningTrailing slash mismatches between cache keys and revalidation calls
- warningRoute rewrites or redirects masking the real cache location
- warningfetch() requests with cache: 'force-cache' or default 'force-cache' behavior in production
- warningMutation happens on one node, but revalidatePath isn’t forwarded to others in multi-region deploy
- warningISR fallback not triggered because there’s no fallback: 'blocking' or 'true' in getStaticPaths
- warningrevalidatePath called after headers sent or in a client component context
Concrete fix directions. Pick the one that matches your root cause.
- buildAlways pass the fully resolved dynamic path to revalidatePath: e.g., revalidatePath(`/blog/${slug}`)
- buildNormalize and match slashes: ensure your revalidatePath argument matches generated route (use /foo, not /foo/ by accident)
- buildAdjust fetch calls to use proper cache mode: cache: 'default' or cache: 'no-store' as needed
- buildMove revalidatePath invocations to server actions or API routes, never client-side
- buildInstrument logs to confirm revalidation triggers and actual path
- buildIf on Vercel/hosted: ensure function regions are set correctly and cache invalidation propagates
A fix you cannot prove is a guess. Close the loop.
- verifiedTrigger revalidatePath, then fetch the affected path and check for updated content
- verifiedReload the page and confirm x-nextjs-cache: MISS header appears once
- verifiedCompare JSON API route data and page data for consistency after mutation
- verifiedCheck Vercel or hosting logs for a new regeneration event on the invalidated path
- verifiedRun a fresh deployment and repeat the workflow to ensure consistency across environments
Things that make this bug worse or harder to find.
- warningPassing route patterns instead of concrete paths to revalidatePath
- warningCalling revalidatePath in a client component or after response is sent
- warningIgnoring trailing slash differences between generated and invalidated paths
- warningAssuming fetch() is always statically cached—double check your cache mode
- warningSkipping verification on both page and API endpoints
- warningNot checking for silent failures in serverless multi-region setups
Stale Blog Post After Publishing New Content on Next.js
Timeline
- 09:02Content writer reports that new post is live in admin panel but not on /blog page.
- 09:04Engineer triggers revalidatePath('/blog/[slug]') after content mutation.
- 09:06QA reloads the blog post page—still sees old content.
- 09:08Engineer checks Vercel logs—no regeneration event for /blog/my-awesome-post.
- 09:11Engineer realizes revalidatePath argument is a route pattern, not the actual slug.
- 09:14Patch deployed: revalidatePath(`/blog/${slug}`) per mutation.
- 09:16Page now shows fresh content after reload; x-nextjs-cache: MISS confirmed.
I was paged early by our content team—they'd published a new blog post, but users kept seeing the previous version. Everything looked fine in the database and the admin preview. I double-checked that our server action called revalidatePath after every publish, but the cache wasn't clearing.
I watched the Vercel deployment logs, expecting Next.js to regenerate the path. No events for the expected route appeared. Meanwhile, users kept reporting they had to wait for the scheduled revalidation interval to see new content.
I dug into the code and spotted the mistake: we were calling revalidatePath('/blog/[slug]'), not the actual path like /blog/my-awesome-post. Once we changed the argument to use the real slug and deployed, the cache started refreshing instantly on each publish.
Root cause
revalidatePath was called with a generic route pattern instead of the specific slug path, so Next.js didn’t find and invalidate the correct cache entry.
The fix
Change revalidatePath('/blog/[slug]') to revalidatePath(`/blog/${slug}`) so each new post's cache entry is actually invalidated.
The lesson
Always pass the fully resolved path (not pattern) to revalidatePath, and use logs to confirm regeneration triggers.
Next.js stores cached content using the fully resolved path as the cache key. Passing a route pattern or an ambiguous route never matches any real cache entry. For instance, /products/[id] is not the key—/products/123 is.
When using revalidatePath, always log the string argument. If you see things like /blog/[slug] in your logs, it’s a bug. The only correct call is with a string like /blog/2024-06-01-my-post.
Next.js routes can be generated with or without trailing slashes, depending on your next.config.js. If you generate /docs/foo but invalidate /docs/foo/, you’ll miss the cache key.
Route rewrites or API middleware can transform the incoming path, so always verify the actual request path matches what you pass to revalidatePath using logs and .next/server/app/pages.json.
Next.js’s fetch() defaults to 'force-cache' for data meant to be statically generated. If you override with 'cache: no-store' you get SSR each time, ignoring ISR and revalidatePath.
If your data source uses 'force-cache', cache is only busted on deploy or explicit invalidation. Inspect fetch configs and test with both cache modes to confirm correct behavior.
In Vercel or similar deployments, revalidatePath can fail silently if not properly propagated across regions. Some setups require additional configuration to ensure all edge locations clear their cache.
Always test revalidation from different physical locations (VPN, proxies) and check function logs in every region for regeneration events.
Frequently asked questions
Can I call revalidatePath from a client component?
No. revalidatePath must run in a server context—server actions, API routes, or getServerSideProps. Client-side calls are ignored or error out.
What’s the right way to invalidate dynamic routes?
Always use the resolved, user-facing path: e.g., revalidatePath(`/blog/${slug}`), never route patterns like /blog/[slug].
Why does my API return fresh data but the page is stale?
You’re likely hitting different cache layers: API routes can be SSR while pages use ISR. Both need explicit cache invalidation if they’re statically generated.
How can I confirm a cache MISS after revalidatePath?
Check the x-nextjs-cache response header—it should say MISS immediately after invalidation and HIT after a reload.