“A fast page is worthless if it's showing content that changed an hour ago.”
Your Sanity blog loads fine in the editor preview. Then real traffic hits it, and every single page view fires a fresh GROQ query straight to the Sanity API. Response times creep up, the Sanity usage dashboard spikes, and nobody actually planned it this way. It just happened, one uncached fetch call at a time.
Sanity CMS caching in Next.js exists to fix exactly that problem before it becomes one. Google's own research found that bounce probability rises by 32% once load time crosses the 3 second mark, and a 2 second delay alone can push bounce rates up by more than 100%. Meanwhile, Next.js now sees somewhere around 40 million weekly npm downloads, so a huge share of Sanity powered sites are already running on the exact caching primitives this guide walks through.
32%
Bounce rate increase from 1 to 3 second load time
1-2
Typical blog posts published per week on a low-change content site
This article walks through a real caching setup for a Sanity CMS blog built on Next.js. It covers Next.js ISR with Sanity, cache tags, revalidateTag, revalidatePath, the Sanity API CDN, and a full Sanity webhook Next.js revalidation flow. The goal of Sanity CMS caching in Next.js is simple: serve most visitors from cache, and only hit the Sanity API when content actually changes or the cache is genuinely empty.
What Sanity CMS Caching in Next.js Actually Means
Sanity CMS caching in Next.js is the practice of storing the result of a GROQ query so the same data does not need to be re-fetched from Sanity on every page view. Instead of calling the Sanity API for every visitor, Next.js stores the response and serves it straight from cache until a TTL expires or a webhook tells it the content changed.
This matters most for content that does not change every second. A blog is a good example. New posts might go live once or twice a week. Categories rarely change. Author bios barely move at all. None of that justifies a live database call on every page load.
The pattern that works well combines two ideas:
- Time-based caching, where each query has a TTL that acts as a safety net
- Event-based invalidation, where a Sanity webhook clears the cache the moment something is actually published
Together, these two ideas mean your Next.js caching strategy does not depend on guessing the right TTL. The webhook does the real work. The TTL just catches anything the webhook might miss.
Why a Read-Heavy Blog Needs a Deliberate Caching Strategy
A typical content site sees far more reads than writes. Thousands of people might view a blog post for every one time an editor updates it. Serving every one of those reads straight from the Sanity API is wasteful, and it adds latency that readers feel immediately.
A proper Next.js caching strategy flips this around. The Next.js Data Cache stores query results locally, close to where the request is handled. Sanity's own CDN adds a second layer behind that. Only when both layers miss does a request reach the actual Sanity API. That's the entire point of Sanity performance optimization: keep the expensive calls rare and the cheap ones common.
The Two Cache Layers Behind Every Sanity Request
Before writing a single line of code, it helps to know exactly what happens between a visitor's click and the page they see. There are two layers doing the heavy lifting here, and they stack on top of each other.

Next.js Data Cache and Sanity CMS Caching
The first layer is the Next.js Data Cache. Every fetch call to Sanity can define a revalidate value and one or more tags. When the same query runs again, Next.js checks this cache before making a network request at all.
export async function getBlogPost(slug: string) { return sanityFetch({ query: postQuery, params: { slug }, tags: ['blog', `blog:post:${slug}`], revalidate: BLOG_TTL.POST, // e.g. 3600 seconds });}This is Next.js Sanity caching at its most basic. No tag, no TTL, means the query falls back to a generic default, which is rarely what you actually want for content that has its own change pattern.
Sanity API CDN as a Second Cache Layer
Behind the Next.js Data Cache sits the Sanity API CDN. Even when Next.js has to refetch because a tag was invalidated or a TTL expired, there's a good chance the CDN already has the answer cached. Only a CDN miss actually reaches the primary Sanity API.
Stacking these two layers is what makes Sanity cache in Next.js genuinely fast at scale. A thousand visitors hitting a popular post should translate into a handful of Sanity API calls, not a thousand.
Next.js ISR with Sanity: How Time-Based Caching Works
Incremental Static Regeneration, or ISR, is the mechanism that lets a page stay statically fast while still updating in the background. Sanity ISR works by pairing a revalidate value with your Sanity fetch calls, so Next.js knows how long a cached response is allowed to stay valid before it's considered stale.
Setting Revalidate Time for Sanity ISR
Picking the right revalidate window depends entirely on how often the underlying content changes. A blog post that gets edited occasionally can tolerate an hour of staleness. A listing page that aggregates recent posts might need a shorter window since it reflects the whole collection.
A practical set of defaults for a low-change blog looks like this:
| Data | TTL | Cache Tags |
|---|---|---|
| Blog post | 1 hour | blog, blog:post:slug |
| Blog listing | 30 minutes | blog, blog:listing |
| Categories / taxonomy | 24 hours | blog, blog:taxonomy |
| Blog settings | 24 hours | blog, blog:settings |
| Slugs / sitemap | 1 hour | blog, blog:slugs |
These numbers are not fixed rules. They should shift if your content-update pattern changes, or if real traffic data suggests something different works better.
Cache Tags: The Key to Targeted Sanity Cache Invalidation
Cache tags give you a way to invalidate exactly the data that changed, instead of wiping the entire cache every time an editor hits publish. Think of them as labels attached to a cached response, so you can later say "clear everything with this label" without touching anything else.
A workable tag structure for a blog usually includes:
blogas a broad, catch-all tagblog:post:<slug>for one specific articleblog:listingfor anything tied to the collection of postsblog:taxonomyfor category datablog:settingsfor site-wide blog settingsblog:slugsfor slug and sitemap related queries
This structure is the backbone of good Sanity cache invalidation. Without it, you're forced to either invalidate too broadly, which wastes cache hits, or too narrowly, which leaves stale data behind.
Next.js revalidateTag for Sanity Content
revalidateTag is the function that actually clears cached data tied to a specific tag. It's the mechanism behind on-demand Sanity cache invalidation, and it's what a webhook calls the moment Sanity tells you something changed.
revalidateTag Sanity Example Code
Here's what calling revalidateTag Sanity style looks like inside an API route:
import { revalidateTag } from 'next/cache';import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { const body = await req.json(); const { _type, slug, operation } = body; if (_type === 'post') { revalidateTag('blog'); revalidateTag('blog:listing'); if (slug) revalidateTag(`blog:post:${slug}`); if (operation === 'create' || operation === 'delete') { revalidateTag('blog:slugs'); } } return NextResponse.json({ revalidated: true, now: Date.now() });} Notice how the tags invalidated depend on what actually changed. A new post touches the listing and slugs. An update to an existing post mostly just touches that one post and the listing. This targeted approach is the whole point of Next.js revalidateTag over a blunt, full-cache wipe.
Next.js revalidatePath vs revalidateTag for Sanity
Next.js also ships revalidatePath, which clears the cache for a specific route rather than a tag. Both do similar jobs, but they solve slightly different problems.
revalidatePath is tied to a URL. If you know exactly which route needs a refresh, and that route maps cleanly to one piece of content, this works well. revalidateTag is tied to data, not a URL, which makes it a better fit for Sanity content that might appear on more than one page at once, like a post that shows up on its own page and inside a listing.
Setting Up a Sanity Webhook Next.js Revalidation Endpoint
A Sanity webhook Next.js integration is what turns your caching strategy from "TTL and hope" into "TTL as a fallback, webhook as the real trigger." Sanity fires a webhook the moment a document is published, updated, or deleted, and that webhook hits a Next.js API route that runs the actual revalidation.
Sanity Webhook Payload and GROQ Projection
The webhook payload should carry just enough information to know what changed. A GROQ projection like this keeps the payload small and predictable:
{ _id, _type, "slug": slug.current, "operation": delta::operation()}delta::operation() tells you whether the change was a create, update, or delete, which matters because each of those should trigger a slightly different Sanity cache invalidation pattern.
Securing the Sanity Webhook Revalidation Route
Any public API route that clears cache needs a way to confirm the request actually came from Sanity and not from a random script hitting your endpoint on a loop.
const signature = req.headers.get('sanity-webhook-signature');if (!isValidSignature(signature, process.env.SANITY_WEBHOOK_SECRET)) { return NextResponse.json({ message: 'Invalid signature' }, { status: 401 });}Webhook Invalidation Rules for Every Sanity Document Change
Different document changes should invalidate different tags. Treating every change as "clear everything" defeats the purpose of having tags in the first place.
A working set of rules looks like this:
- Post created: invalidate
blog,blog:listing,blog:slugs - Post updated: invalidate
blog,blog:post:<slug>,blog:listing - Post deleted or unpublished: invalidate
blog,blog:post:<slug>,blog:listing,blog:slugs - Category changed: invalidate
blog:taxonomyonly - Blog settings changed: invalidate
blog:settingsonly
Categories and settings rarely change, so there's no reason to touch the rest of the cache when they do. This is a small detail, but it's the difference between a cache that stays warm and one that keeps resetting itself for no reason.
Recommended Sanity Cache TTL Policy by Content Type
TTLs act as your safety net. If a webhook ever fails to fire, or an edge case slips through, the TTL guarantees the cache eventually refreshes on its own anyway.
Author data deserves a mention here too. It changes rarely, so it can safely use a long TTL, following the same tagging principles as everything else rather than being treated as a special case.
Sanity CMS Performance Optimization for High Traffic
Sanity performance optimization at scale is really about making sure the expensive path, the actual Sanity API call, is the rare exception rather than the norm.
Next.js High Traffic Optimization Checklist
A high-traffic Sanity blog benefits from a few consistent habits applied across every query, not just the popular ones.
- Give every Sanity query an explicit TTL and tag set, never rely on a generic default
- Keep cache configuration next to the query itself, so it's obvious what each fetch is doing
- Reuse existing tag and TTL constants instead of inventing new ones per feature
- Keep preview or draft fetching completely separate from public, cached fetching
- Avoid TTLs shorter than the content actually needs, since overly short windows just create unnecessary Sanity API calls
Under this setup, a spike of a thousand visitors to one article mostly hits the Next.js Data Cache, with only an occasional miss reaching the Sanity API CDN, and rarer still, the Sanity API itself. That's the target shape for Next.js high traffic optimization on any read-heavy content site.
Common Mistakes When Caching Sanity Data in Next.js
A few patterns show up again and again in Sanity Next.js caching setups that don't quite work as intended.
The first is skipping tags entirely and relying only on a TTL. This means every content change has to wait out the full TTL window before readers see it, which defeats the purpose of having a CMS with instant publishing in the first place.
The second is invalidating too broadly. Clearing the entire blog tag on every single change, even a minor settings tweak, forces far more Sanity API calls than necessary and undoes a lot of the benefit of Sanity CMS caching in Next.js.
The third is mixing preview and production fetching logic. Draft content needs its own fetching path so that editing a draft never accidentally invalidates or pollutes the public, cached version of a page.
Teams working through this kind of setup often lean on structured backend development practices to keep the revalidation logic, webhook handling, and query layer organized as the content model grows. It's the same discipline that shows up in a headless CMS project built with a Next.js admin panel, where content, caching, and delivery all had to be planned around each other from the start rather than bolted on later.
Frequently Asked Questions
Conclusion
Sanity CMS caching in Next.js is not about picking one clever trick. It's about layering a few simple ideas together. The Next.js Data Cache handles most requests. The Sanity API CDN catches most of what's left. Cache tags keep invalidation precise instead of all-or-nothing. And a Sanity webhook Next.js integration makes sure published changes show up almost immediately, with TTLs quietly covering anything that slips through.
For a read-heavy, low-change blog, that combination means most visitors never actually wait on Sanity at all. They get a fast, cached page, and the content stays accurate the moment an editor hits publish.
Topics
- #Next.js
- #Sanity
- #Webhooks
- #Sanity CDN
- #Caching

