
Sanity CDN vs Live API: When to Use Each
Use the Sanity API CDN (useCdn: true, served from apicdn.sanity.io) for read queries that serve visitors: browser fetches, high-traffic server rendering and anything where a short delay before new content appears is acceptable. Use the live API (useCdn: false, served from api.sanity.io) when you need the latest saved content: static builds, webhook-triggered revalidation, draft previews, scripts that read and then write, and every mutation. The CDN is faster, cheaper and scales without rate limits on cache hits; the live API is always fresh but slower and more tightly limited.
The choice is one boolean in your client config, but it affects freshness, cost, rate limits and even whether your preview shows drafts. Getting it wrong usually shows up as "I published but the site didn't update" or a surprise overage on API requests. This article explains what each endpoint does, how caching works on the CDN, a decision table for common scenarios, how tokens and perspectives interact with useCdn, how this differs from the Live Content API, and how to configure clients so each request uses the right endpoint.
Two Endpoints, One API
Every Sanity project exposes the same query API on two hosts:
| Live API | API CDN | |
|---|---|---|
| Host | <projectId>.api.sanity.io | <projectId>.apicdn.sanity.io |
| Client setting | useCdn: false | useCdn: true |
| Freshness | Latest saved content, every time | Cached; new content appears after a short delay |
| Speed | Every request reaches the Content Lake | Cache hits are served from the edge |
| Rate limits | Concurrency and per-IP limits apply | Cache hits aren't rate limited |
| Supports | Queries, mutations, listeners, everything | GROQ queries and GraphQL only |
| Quota | Counts as API requests | Counts as API CDN requests |
The query syntax, response format and API versions are identical. Only the host changes. That's why most clients reduce the whole decision to a useCdn flag.
A note on naming: Sanity's docs call api.sanity.io the live, uncached API. That's different from the Live Content API, which is a real-time event stream your front end subscribes to. We'll come back to how they relate.
How the API CDN Caches
The API CDN is built for end-user read traffic, and its cache rules reflect that:
- Cached: GET and HEAD requests, plus POST requests to
/data/queryand/graphql, because those endpoints are read-only. POST bodies are limited to 300 KB. - Not cached: non-200 responses and responses larger than 10 MB.
- Rejected: any other POST, since it could contain a mutation. Send writes to the live API.
- Authenticated requests are cached per token: a request with a bearer token gets its own cache segment, so different tokens never share results. Cookies are ignored.
- Listeners are redirected: real-time listeners and the Live Content API always go to the live API.
- Resilience: if the Content Lake is unavailable, the CDN can keep returning the last cached content for up to two hours.
Caching is keyed on the full request URL, including query parameters. Two requests with a different query string, parameter value or API version are two cache entries. Queries built with stable text and GROQ parameters therefore cache far better than queries with values interpolated into them, which is one more reason to follow the GROQ parameters guide.
When content changes, cached responses are updated after a short propagation delay rather than instantly. Sanity doesn't publish a fixed number for this, so design for "a few moments" of staleness, not zero.
Which One Should You Use?
| Scenario | useCdn | Reason |
|---|---|---|
| Browser fetches (client components, SPAs) | true | Fast, scales with traffic, no token needed for public data |
| Server rendering for high-traffic pages | true | Avoids live API rate limits under load |
Static generation and generateStaticParams | false | A build should capture the latest content |
| Revalidation triggered by a webhook | false | The page must rebuild from the content that triggered it |
| Draft Mode, previews, Visual Editing | false | Drafts need a token and the latest edits |
| Scripts that read, then patch | false | Never base a write on cached data |
| Mutations | Not applicable | Always sent to the live API |
| Sitemaps, feeds, search indexing jobs | false (or true with a short cache) | Usually run rarely and need recent content |
When in doubt, Sanity's own guidance is to default to useCdn: true and switch specific requests to false.
Why Static Builds Should Skip the CDN
A build runs once and the result is served for hours or days. If it reads from a cache that's a few moments behind, the page you just published might be missing from a build that started because you published it. The live API avoids that. The trade-off is rate limits: a large static site that fetches thousands of pages in parallel can hit the live API's concurrency limit, so cap parallel requests during builds. The static site with Astro and Sanity post shows a build-time setup.
Why Webhook Revalidation Should Skip the CDN
A webhook fires when content changes. If your handler revalidates a page and the page immediately refetches from the CDN, it can cache the old version again. Fetch with useCdn: false in revalidated code paths, or add a short delay. parseBody from next-sanity/webhook waits a few seconds by default for exactly this reason. The webhook rebuild guide covers the full flow.
Configure Clients for Both
Create one base client with the CDN enabled, then derive variants with withConfig():
// src/sanity/lib/client.ts
import { createClient } from 'next-sanity'
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2025-02-19',
useCdn: true,
perspective: 'published',
})
// Fresh reads for builds, revalidation and scripts
export const freshClient = client.withConfig({ useCdn: false })
withConfig() returns a new client and leaves the original untouched, so you can pick per call site:
// src/app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { client, freshClient } from '@/sanity/lib/client'
import { defineQuery } from 'next-sanity'
const SLUGS_QUERY = defineQuery(`*[_type == "post" && defined(slug.current)].slug.current`)
const POST_QUERY = defineQuery(`*[_type == "post" && slug.current == $slug][0]{ title, body }`)
export async function generateStaticParams() {
const slugs = await freshClient.fetch(SLUGS_QUERY)
return slugs.map((slug) => ({ slug }))
}
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await client.fetch(POST_QUERY, { slug })
if (!post) notFound()
return <h1>{post.title}</h1>
}
You can also override per request with the third argument to fetch():
const latest = await client.fetch(POST_QUERY, { slug }, { useCdn: false })
If you use sanityFetch from defineLive in next-sanity, it chooses for you: requests with the published perspective use the CDN, and other perspectives, such as drafts, bypass it. The next-sanity guide covers that helper in detail.
Tokens, Perspectives and the CDN
Older versions of @sanity/client silently skipped the CDN whenever a token was set. Current versions don't: authenticated requests go to the CDN when useCdn is true, with a cache segment per token. That's useful for private datasets, which can be served from the API CDN as long as each request carries a token, but it also means you should be deliberate:
- Draft previews: use
useCdn: falsewith thedraftsperspective. Editors expect to see what they typed a second ago. - Private datasets on public pages: fetch on the server with a Viewer token and
useCdn: true, and cache the rendered page. Never put the token in the browser. - One token per purpose: because the cache is segmented by token, sharing a token across unrelated apps doesn't improve hit rates and makes revocation harder.
Remember the default perspective. With apiVersion set to 2025-02-19 or later, queries return the published perspective unless you ask otherwise, so drafts never leak into CDN responses by accident. The Content Lake post explains perspectives.
Quotas and Rate Limits
The two endpoints are metered separately, and the CDN allowance is much larger. On the Free and Growth plans, the included API CDN request quota is four times the live API quota, as listed in the Sanity pricing breakdown. A site that accidentally sends all public traffic to the live API burns through its quota and, on Growth, pays overages.
The live API also enforces limits that cached CDN hits avoid:
- Concurrency per dataset: 500 queries and 100 mutations in flight at once.
- Per IP: 500 API calls, 25 mutations and 25 uploads per second.
Exceeding a limit returns HTTP 429. @sanity/client retries rate-limited queries with exponential backoff, but it doesn't retry mutations, so batch writes into transactions and limit their concurrency yourself.
Where the Live Content API Fits
The Live Content API solves a different problem: telling your front end that content has changed. Clients subscribe to sync tags, identifiers returned with query results, and receive an event when content behind those tags changes. Your app then refetches only the affected queries.
It complements the CDN rather than replacing it:
- Pages fetch with the published perspective through the CDN.
- A live connection listens for changes to the sync tags those queries returned.
- When an editor publishes, the event arrives, the app revalidates, and the refetch picks up the new content.
In Next.js, defineLive and the <SanityLive /> component wire this up and handle caching for you. The Live Content API itself always connects to the live API, never the CDN. For a full walkthrough, see Using Live Preview With Sanity, and for layering it with framework caches, Improving Site Performance With Sanity Caching.
Check Which Endpoint You're Hitting
When freshness looks wrong, confirm the host in the network tab or your server logs:
https://abc123.apicdn.sanity.io/v2025-02-19/data/query/production?query=... # CDN
https://abc123.api.sanity.io/v2025-02-19/data/query/production?query=... # live API
You can also test with curl, which is handy for comparing responses straight after publishing:
curl -s "https://abc123.api.sanity.io/v2025-02-19/data/query/production?query=*%5B_type%3D%3D%22post%22%5D%5B0%5D%7Btitle%7D"
curl -s "https://abc123.apicdn.sanity.io/v2025-02-19/data/query/production?query=*%5B_type%3D%3D%22post%22%5D%5B0%5D%7Btitle%7D"
If the live API shows the new title and the CDN doesn't, wait a moment. If both are stale, the problem is elsewhere: perhaps the document wasn't published, or your framework's own cache is serving an old page. The HTTP API post covers the raw endpoints.
Common Mistakes
useCdn: falseeverywhere "to be safe": it makes the site slower, uses the smaller quota and exposes you to rate limits under traffic.useCdn: truein revalidation handlers: the rebuilt page can cache stale content again.- Previews through the CDN: editors see old drafts and lose trust in the preview.
- Reading from the CDN before writing: patches based on cached data can overwrite newer edits. Use the live API and
ifRevisionIdfor safe writes. - Unstable query URLs: interpolating timestamps or random values into queries defeats the cache. Use parameters with stable values.
FAQ: Sanity CDN vs Live API
It chooses the host. With useCdn true, read queries go to apicdn.sanity.io, the cached API CDN. With useCdn false, they go to api.sanity.io, the live, uncached API. Mutations and listeners always use the live API whatever the setting.
Briefly. Cached responses update a short time after content changes, but Sanity doesn't guarantee a fixed delay. If a request must reflect content saved a second ago, such as a preview or a webhook-triggered rebuild, use the live API.
Yes. Current clients send authenticated requests to the CDN when useCdn is true, and the cache is segmented per token. That makes the CDN suitable for private datasets queried from the server. Keep the token server-side.
No. The live API is the uncached query endpoint at api.sanity.io. The Live Content API is a real-time event stream that notifies your app when content behind its queries changes, so it knows when to refetch. They work together with the CDN rather than replacing it.
Generally no. A build should capture the latest published content, so use useCdn false. For very large sites, limit concurrent requests during the build so you stay under the live API's concurrency limit.
No. The API CDN caches query responses. Images and files are served from a separate asset CDN at cdn.sanity.io, which is always used for assets regardless of the useCdn setting.
Conclusion
The API CDN and the live API return the same data from the same query language. The difference is freshness versus scale. Route public, read-only traffic through the CDN with useCdn: true, and switch to useCdn: false for builds, revalidation, previews and any read that feeds a write. Configure one base client with the CDN on and derive a fresh variant with withConfig(), so each call site makes the right choice explicitly.
Add the Live Content API when you want pages to update as soon as editors publish, and keep query URLs stable so the cache can do its job. With those pieces in place, your site stays fast under traffic, stays inside its quotas and still shows new content quickly.


