> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Diagnose an API request spike

Find out why API request usage jumped: check your client's useCdn setting, then use request logs to find the caller, tag, or query.

Sanity meters API requests and API CDN requests separately. Requests to your project's `api.sanity.io` host count toward the API requests quota, and requests to its `apicdn.sanity.io` host count toward the API CDN requests quota. See [Plans and payments](https://www.sanity.io/docs/platform-management/plans-and-payments) for how each quota is defined.

When API requests grow much faster than your site's traffic, check two things: whether content that could come from the API CDN is being fetched from the live API instead, and whether a caller you don't expect is querying your project. This guide shows how to check your client configuration and how to use request logs to find the caller, request tag, or GROQ query behind the requests.

Prerequisites:

- Access to your project in [Manage](https://www.sanity.io/manage) at sanity.io/manage, to see usage and download request logs.
- `jq`, to run the log analysis commands in this guide.
- An application that queries your project with `@sanity/client`, for the configuration fix.

## Check which quota is growing

In Manage, select your project and open the **Usage** tab. Compare API requests with API CDN requests for the current month. If API requests are rising and API CDN requests aren't, the extra requests are reaching the live API. Check your client configuration first, then use request logs to confirm which caller sends them.

Requests from Sanity Studio and other Sanity applications don't count toward API or bandwidth usage, and neither do responses with a 4xx or 5xx status code. See the [Request logs data reference](https://www.sanity.io/docs/platform-management/reference-request-log-data) for the full list of exempt callers.

## Use the API CDN for published content

`@sanity/client` sends queries to the API CDN when `useCdn` is `true`, and to the live API when it's `false`. If you don't set `useCdn`, the client treats it as `true` and logs a console warning.

A client with `useCdn: false` that fetches published content on every server render sends a live API request for every query on every page view. API request usage then grows with your traffic instead of being absorbed by the cache. Use separate clients for published and draft content:

```typescript
import {createClient} from '@sanity/client'

const config = {
  projectId: 'YOUR_PROJECT_ID',
  dataset: 'production',
  apiVersion: '2025-02-19',
}

// Published content for every page view: cached on the API CDN
export const client = createClient({
  ...config,
  useCdn: true,
})

// Draft content for preview and draft mode only: always uncached
export const previewClient = createClient({
  ...config,
  useCdn: false,
  perspective: 'drafts',
  token: process.env.SANITY_API_READ_TOKEN,
})
```

Replace `YOUR_PROJECT_ID` with your project ID. `SANITY_API_READ_TOKEN` is an environment variable holding a token with read access, which the preview client needs to fetch drafts. Keep the token on the server.

Use `client` for every page view, and `previewClient` only in preview or draft mode. Draft content can't be served from the API CDN: when the perspective is `drafts` or an array of perspectives, the client sends the query to the live API even if `useCdn` is `true`, and logs this warning:

```text
The Sanity client is configured with the `perspective` set to `drafts` or `previewDrafts`, which doesn't support the API-CDN. The Live API will be used instead. Set `useCdn: false` in your configuration to hide this warning.
```

`useCdn: false` is still the right setting where request volume doesn't scale with visitors, such as static site builds, where you want the freshest content at build time. See [API CDN](https://www.sanity.io/docs/content-lake/api-cdn) for the cache policy.

## Download your request logs

On self-serve plans, download a request log export from the **Usage** tab of your project in Manage. The export is a compressed newline-delimited JSON (NDJSON) file covering up to 7 days, ending the day before you download it, and is capped at 1 GB. Large projects get a truncated file that covers fewer days. See [Request logs](https://www.sanity.io/docs/platform-management/request-logs) for export limits and for log delivery on Enterprise plans.

The commands in this guide read the export as `request-logs.ndjson.gz`. Rename your file or change the commands to match.

## Count billable requests by API

This command counts requests that returned a status below 400, skips OPTIONS requests and requests tagged with the `sanity.` prefix that Sanity's own applications use, and groups the rest by the `domain` field. `api` is the live API, `apicdn` is the API CDN, and `cdn` is asset traffic:

```sh
gunzip --stdout request-logs.ndjson.gz \
  | jq -r 'select(.body.status < 400 and .body.method != "OPTIONS")
           | select(any(.attributes.sanity.tags[]?; startswith("sanity.")) | not)
           | .attributes.sanity.domain' \
  | sort | uniq -c | sort -rn
```

If `api` has far more requests than you expect, find out what sends them.

## Find which caller sends live API requests

This command groups live API requests by HTTP method, endpoint, and user agent, so you can tell your own server, browsers, and other integrations apart:

```sh
gunzip --stdout request-logs.ndjson.gz \
  | jq -r 'select(.attributes.sanity.domain == "api" and .body.status < 400 and .body.method != "OPTIONS")
           | select(any(.attributes.sanity.tags[]?; startswith("sanity.")) | not)
           | "\(.body.method)\t\(.attributes.sanity.endpoint)\t\(.body.userAgent)"' \
  | sort | uniq -c | sort -rn | head -10
```

A large group of `query` requests from your server's user agent points at a server-side client configured with `useCdn: false`. A user agent you don't recognize points at a crawler or an integration calling your project directly. The `remoteIp` and `referer` fields in the same log lines help identify it.

## Find which part of your application sends requests

If your application sets request tags, group live API requests by tag. Requests without a tag appear as `(untagged)`:

```sh
gunzip --stdout request-logs.ndjson.gz \
  | jq -r 'select(.attributes.sanity.domain == "api" and .body.status < 400 and .body.method != "OPTIONS")
           | select(any(.attributes.sanity.tags[]?; startswith("sanity.")) | not)
           | if (.attributes.sanity.tags // []) == [] then "(untagged)"
             else .attributes.sanity.tags[] end' \
  | sort | uniq -c | sort -rn | head -10
```

Untagged requests can't be traced to a part of your application this way. Set `requestTagPrefix` on each client and a `tag` on individual queries so future exports show where requests come from. See [Request tags for filtering logs in @sanity/client](https://www.sanity.io/docs/apis-and-sdks/js-client-request-tags).

## Find the GROQ queries sending the most requests

Queries sent as GET requests carry a `groqQueryIdentifier`, a hash of the query string without parameters. This command counts live API requests per query identifier:

```sh
gunzip --stdout request-logs.ndjson.gz \
  | jq -r 'select(.attributes.sanity.domain == "api" and .body.status < 400 and .body.method != "OPTIONS")
           | select(any(.attributes.sanity.tags[]?; startswith("sanity.")) | not)
           | (.attributes.sanity.groqQueryIdentifier // "")
           | select(. != "")' \
  | sort | uniq -c | sort -rn | head -10
```

To see the query behind an identifier, print one request URL for it. Replace `QUERY_IDENTIFIER` with a value from the previous command's output. The `query` parameter in the URL holds the URL-encoded GROQ query:

```sh
gunzip --stdout request-logs.ndjson.gz \
  | jq -r --arg id "QUERY_IDENTIFIER" \
      'select(.attributes.sanity.groqQueryIdentifier == $id) | .body.url' \
  | head -1
```

Queries sent as POST requests have an empty `groqQueryIdentifier` and no query in the URL, so they don't appear in this breakdown. `@sanity/client` sends a query as POST when the URL-encoded query and its parameters reach 11,264 characters. To attribute POST queries, give them request tags. POST queries to `/data/query` can still be cached on the API CDN, so `useCdn: true` applies to them too.

## Related resources

- [API CDN](https://www.sanity.io/docs/content-lake/api-cdn): cache policy, rate limits, and when to use the live API.
- [Request logs data reference](https://www.sanity.io/docs/platform-management/reference-request-log-data): every field in a request log line.
- [Understanding quotas](https://www.sanity.io/docs/help/understanding-quotas): how quotas reset and what happens when you exceed them.
- [Next.js 16 and SanityLive: avoiding request overages](https://www.sanity.io/docs/help/nextjs-16-sanitylive-status): request growth after upgrading to Next.js 16 on next-sanity v12.

