Your editors click publish in Sanity, refresh the page, and see the old content. They publish again, then message you. A Next.js and Sanity site that serves stale content after publishing is almost always a revalidation wiring problem, not a Sanity problem, and it is fixable without a rebuild.
What the symptoms usually look like
The pattern tells you a lot about the cause:
- Content updates only after a redeploy. That points to fully static pages with no revalidation at all.
- Content updates after an hour or a day. That is a time-based
revalidatevalue doing the work, and the webhook is missing or failing. - Some pages update and others do not. That is usually a tag or path mismatch: the webhook revalidates what it knows about, and the stale pages are cached under something else.
- It works in Studio preview but not on the public URL. Draft mode bypasses the cache, so preview looks fine while production is stale.
- It updates for you but not for editors in another country. That suggests a CDN or browser caching layer in front of the app.
What it costs to ignore it
Stale content is not just annoying. Editors stop trusting the Studio, start asking developers to redeploy for every typo, and your marketing team loses the main reason you went headless. Price changes, legal copy, and campaign pages that show old information carry real risk. Some teams react by setting revalidation to zero, which trades the problem for slow pages and a larger API bill. I covered that second failure in Sanity API bill spiking? Why requests explode on Next.js sites.
Cause 1: the webhook never fires or fails silently
Open the Sanity project settings, go to the API section, and look at your webhook and its delivery attempts. Check three things: the filter, the projection, and the response status. A common mistake is a filter limited to one document type, so new types added later never trigger a refresh. Another is the endpoint URL pointing at a preview deployment or an old domain.
If deliveries show 401 or 403, your secret verification is rejecting valid requests. If they show 404, the route handler path is wrong. The full verified setup is in Sanity webhooks and Next.js ISR revalidation with HMAC verification.
Cause 2: the revalidation tag does not match the fetch tag
This is the most common bug I see on inherited codebases. The webhook calls revalidateTag with one string, and the fetch was cached with a different one.
// lib/sanity/fetch.ts - broken: tag names drift apart
import { client } from './client'
export function getPost(slug: string) {
return client.fetch(
`*[_type == "post" && slug.current == $slug][0]`,
{ slug },
{ next: { tags: [`post-${slug}`] } }
)
}
// app/api/revalidate/route.ts - revalidates a tag nobody uses
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
const body = await req.json()
revalidateTag(body._type) // "post", but fetches use "post-my-slug"
return Response.json({ ok: true })
}
The fix is one naming convention used in both places. Tag by document type, and optionally by type plus slug, and have the webhook send both.
// lib/sanity/tags.ts
export const tagsFor = (type: string, slug?: string) =>
slug ? [type, `${type}:${slug}`] : [type]
Then use tagsFor in every fetch and in the route handler. Be aware that the exact signature of revalidateTag has changed across recent Next.js majors, so check the Next.js 16 docs for the version you run rather than copying an older snippet. If webhooks fire in bursts, batching helps; see Sanity webhook revalidation: batching tags in Next.js.
Cause 3: references and parent pages are not invalidated
An editor changes an author name or a navigation item. The author document revalidates, but the 200 blog posts that embed that author stay cached. Either tag every query with the referenced types it reads, or send the webhook a list of affected documents using a GROQ projection that follows references. Without this, the site looks randomly stale.
Cause 4: a cache layer in front of Next.js
If Next.js revalidates correctly but visitors still see old HTML, look upstream. A CDN, a proxy, or an overly aggressive Cache-Control header can pin the page. Compare the response headers from the origin and from the public domain. If they differ, that is your answer.
Cause 5: the client uses the CDN API for reads that need freshness
The Sanity API CDN can serve slightly older data. For server-side fetches triggered by a webhook, the content may not have propagated yet when you revalidate. Using the live API without the CDN for those server fetches avoids the race. Confirm the option name in the current @sanity/client documentation before changing it.
How to verify the fix
- Publish a small change in Studio, such as a headline edit.
- Check the webhook delivery log and confirm a 200 response.
- Reload the public page in a private window and confirm the change.
- Repeat with a referenced document, such as an author, and confirm dependent pages update.
- Check response headers on the public domain for unexpected caching.
Add a lightweight log line in the route handler that records the document type, slug, and tags revalidated. When an editor reports a problem next month, you can answer in a minute instead of guessing.
What to ask a developer
- How do you name cache tags, and where is that convention documented?
- What happens when a referenced document changes?
- Can I see the webhook delivery log and the route handler code?
- How do you test revalidation before launch?
- Will you fix it without disabling caching?
A good answer names a tag convention and a test. A bad answer is "we set revalidate to 10 seconds."
Work I do
Diagnosing and fixing stale-content problems on Sanity and Next.js sites is work I do regularly, usually as part of a Performance and Sanity audit from $800. If your editors have stopped trusting the publish button, that is a good place to start.
Top comments (0)