LoadForge LogoLoadForge

Sanity Load Testing: Performance Testing Sanity APIs with LoadForge

Sanity Load Testing: Performance Testing Sanity APIs with LoadForge

Introduction

Sanity Load Testing is essential when your headless CMS powers high-traffic storefronts, content-rich marketing sites, or personalized digital experiences. Sanity is often placed directly in the critical path of modern e-commerce and CMS architectures, serving product content, landing pages, blog posts, navigation data, and search-driven experiences through its Content Lake APIs. If those APIs slow down under traffic, the result is not just a backend issue—it becomes a customer experience problem.

With LoadForge, you can run realistic Sanity load testing, performance testing, and stress testing scenarios using Locust-based Python scripts. That means you can simulate anonymous content reads, authenticated preview traffic, mutation-heavy editorial workflows, and image asset requests from distributed cloud infrastructure. You can also monitor results in real time, scale tests globally, and integrate them into CI/CD pipelines to catch regressions before they affect production.

In this guide, you’ll learn how to load test Sanity APIs with LoadForge, what bottlenecks to watch for, and how to build practical Locust scripts that reflect real Sanity usage patterns.

Prerequisites

Before you start performance testing Sanity with LoadForge, make sure you have:

  • A Sanity project ID
  • Your dataset name, such as production or staging
  • A Sanity API version, for example 2023-10-01
  • A read token if you need to test authenticated or private dataset access
  • A write token if you want to test mutations
  • A list of realistic GROQ queries used by your frontend, e-commerce app, or CMS clients
  • Access to the endpoints you want to test:
    • Query API
    • Mutate API
    • Assets API
    • Image URLs if applicable
  • A LoadForge account to run distributed load tests and analyze performance results

You should also understand whether you are testing:

  • Public CDN-backed reads
  • Authenticated API reads
  • Preview or draft content access
  • Editorial writes and mutations
  • Asset-heavy content delivery patterns

For best results, create a dedicated staging dataset or a safe test environment. Avoid running mutation-heavy stress tests against production content unless you have strict safeguards in place.

Understanding Sanity Under Load

Sanity behaves differently from a traditional monolithic CMS because it is API-first and designed around a hosted content platform. That changes how performance testing should be approached.

Key Sanity traffic patterns

Most Sanity load testing falls into one or more of these categories:

  • Read-heavy API traffic
    Frontends query documents using GROQ through endpoints like /v2023-10-01/data/query/production
  • Preview traffic
    Authenticated users request draft content or uncached data
  • Mutation traffic
    Editors or automated systems create, update, and patch documents
  • Asset delivery
    Images and files are requested through Sanity’s CDN and asset pipeline
  • Mixed workloads
    E-commerce and CMS platforms often combine navigation, category, product, landing page, and search content requests in a single user journey

Common bottlenecks in Sanity performance testing

When load testing Sanity APIs, developers often discover these issues:

  • Complex GROQ queries causing slower response times
  • Large document projections returning too much data
  • High-cardinality filters that increase query cost
  • Preview mode traffic bypassing caching advantages
  • Frequent mutations creating contention or rate-limit exposure
  • Frontend request waterfalls where one page triggers many Sanity API calls
  • Unoptimized image usage causing excessive bandwidth and latency

What to measure

For Sanity performance testing, focus on:

  • Median and p95 response times
  • Error rate under load
  • Throughput in requests per second
  • Behavior under sustained concurrency
  • Performance differences between cached and authenticated requests
  • Latency spikes during mixed read/write workloads

LoadForge makes this easier by giving you real-time reporting, distributed testing from cloud infrastructure, and the ability to simulate traffic from global test locations.

Writing Your First Load Test

A good first Sanity load test should simulate the most common behavior: frontend content retrieval using a GROQ query. This usually represents homepage, landing page, category page, or product listing traffic.

Basic Sanity query load test

python
from locust import HttpUser, task, between
from urllib.parse import quote
 
class SanityReadUser(HttpUser):
    wait_time = between(1, 3)
 
    project_id = "abc123xy"
    dataset = "production"
    api_version = "2023-10-01"
 
    @task
    def fetch_homepage_content(self):
        groq_query = """
        *[_type == "homepage" && slug.current == "home"][0]{
          title,
          hero{
            heading,
            subheading,
            ctaText,
            ctaLink
          },
          featuredProducts[]->{
            _id,
            title,
            slug,
            price,
            "mainImage": mainImage.asset->url
          }
        }
        """
 
        encoded_query = quote(groq_query)
        url = f"/v{self.api_version}/data/query/{self.dataset}?query={encoded_query}"
 
        with self.client.get(
            url,
            name="Sanity Query - Homepage",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure(f"Unexpected status code: {response.status_code}")
                return
 
            data = response.json()
            if "result" not in data:
                response.failure("Missing result field in Sanity response")
                return
 
            response.success()

Why this script is useful

This script tests a realistic Sanity read pattern:

  • It calls the Sanity Query API
  • It uses a GROQ query that joins referenced product documents
  • It validates that the API returns a successful JSON structure
  • It labels the request clearly in Locust metrics

Running this in LoadForge

In LoadForge, set the host to your Sanity API domain:

bash
https://abc123xy.api.sanity.io

This test helps establish a baseline for:

  • Query latency
  • Throughput for common content reads
  • Error behavior under moderate concurrency

If your homepage depends on Sanity, this is the first place to begin load testing.

Advanced Load Testing Scenarios

Once you have a baseline, move on to more realistic Sanity performance testing scenarios. In production, traffic is rarely a single query repeated forever. You usually need a mix of public reads, authenticated preview requests, and content mutations.

Scenario 1: Testing product listing and product detail queries

This is especially relevant for e-commerce and CMS setups where Sanity stores product content, merchandising blocks, and category pages.

python
from locust import HttpUser, task, between
from urllib.parse import quote
import random
 
class SanityEcommerceUser(HttpUser):
    wait_time = between(1, 2)
 
    project_id = "abc123xy"
    dataset = "production"
    api_version = "2023-10-01"
 
    category_slugs = ["new-arrivals", "mens-shoes", "womens-jackets", "accessories"]
    product_slugs = ["air-runner-2", "urban-shell-jacket", "canvas-weekender", "trail-cap"]
 
    def run_query(self, query, name):
        encoded_query = quote(query)
        url = f"/v{self.api_version}/data/query/{self.dataset}?query={encoded_query}"
 
        with self.client.get(url, name=name, catch_response=True) as response:
            if response.status_code != 200:
                response.failure(f"Status {response.status_code}")
                return
 
            payload = response.json()
            if "result" not in payload:
                response.failure("Sanity result missing")
                return
 
            response.success()
 
    @task(3)
    def category_page_query(self):
        slug = random.choice(self.category_slugs)
        query = f'''
        *[_type == "category" && slug.current == "{slug}"][0]{{
          title,
          description,
          seoTitle,
          seoDescription,
          "products": *[_type == "product" && references(^._id)] | order(priority asc)[0...24]{{
            _id,
            title,
            slug,
            price,
            inStock,
            badge,
            "imageUrl": mainImage.asset->url
          }}
        }}
        '''
        self.run_query(query, "Sanity Query - Category Page")
 
    @task(2)
    def product_detail_query(self):
        slug = random.choice(self.product_slugs)
        query = f'''
        *[_type == "product" && slug.current == "{slug}"][0]{{
          _id,
          title,
          slug,
          description,
          price,
          sku,
          inStock,
          variants[]{{
            _key,
            title,
            sku,
            price
          }},
          specifications[]{{
            label,
            value
          }},
          "relatedProducts": *[_type == "product" && references(^._id)][0...4]{{
            title,
            slug,
            price
          }},
          "imageUrl": mainImage.asset->url
        }}
        '''
        self.run_query(query, "Sanity Query - Product Detail")

What this scenario tests

This script is more realistic because it simulates:

  • Category page traffic
  • Product detail page traffic
  • Reference expansion
  • Larger projections and nested arrays

This is where you often uncover slow GROQ queries, especially if category pages pull too many fields or dereference too many related documents.

Scenario 2: Testing authenticated preview and draft content access

Preview mode is one of the most important Sanity load testing scenarios because it often behaves differently from public traffic. Authenticated requests may bypass CDN efficiencies and expose the true cost of complex queries.

python
from locust import HttpUser, task, between
from urllib.parse import quote
import os
 
class SanityPreviewUser(HttpUser):
    wait_time = between(2, 5)
 
    dataset = "staging"
    api_version = "2023-10-01"
    token = os.getenv("SANITY_READ_TOKEN")
 
    def on_start(self):
        self.headers = {
            "Authorization": f"Bearer {self.token}",
            "Accept": "application/json"
        }
 
    @task
    def preview_landing_page(self):
        groq_query = """
        *[_type == "landingPage" && slug.current == "spring-sale"][0]{
          title,
          slug,
          hero,
          contentBlocks[]{
            ...,
            _type == "productGrid" => {
              "products": products[]->{
                _id,
                title,
                price,
                slug,
                "imageUrl": mainImage.asset->url
              }
            }
          },
          seo
        }
        """
 
        encoded_query = quote(groq_query)
        url = f"/v{self.api_version}/data/query/{self.dataset}?perspective=previewDrafts&query={encoded_query}"
 
        with self.client.get(
            url,
            headers=self.headers,
            name="Sanity Preview Query - Landing Page",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure(f"Preview request failed: {response.status_code}")
                return
 
            data = response.json()
            if "result" not in data:
                response.failure("Preview response missing result")
                return
 
            response.success()

Why preview testing matters

If your editors, merchandisers, or content teams rely on preview environments, you need to know:

  • How fast draft content loads under concurrent usage
  • Whether authenticated requests produce latency spikes
  • Whether complex landing pages become slow when previewing unpublished changes

This is a common blind spot in Sanity performance testing.

Scenario 3: Testing mutations and editorial workflows

Sanity is not just read-heavy. Many teams also need to validate how it behaves during bursts of content writes, especially during catalog imports, campaign launches, or editorial publishing windows.

python
from locust import HttpUser, task, between
import os
import uuid
import random
 
class SanityMutationUser(HttpUser):
    wait_time = between(1, 3)
 
    dataset = "staging"
    api_version = "2023-10-01"
    token = os.getenv("SANITY_WRITE_TOKEN")
 
    def on_start(self):
        self.headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }
 
    @task(2)
    def create_promo_banner(self):
        banner_id = f"promo-banner-{uuid.uuid4()}"
        payload = {
            "mutations": [
                {
                    "create": {
                        "_id": banner_id,
                        "_type": "promoBanner",
                        "title": f"Flash Sale {random.randint(100, 999)}",
                        "slug": {
                            "_type": "slug",
                            "current": f"flash-sale-{random.randint(1000,9999)}"
                        },
                        "headline": "Save up to 30% this weekend",
                        "ctaText": "Shop Now",
                        "ctaLink": "/collections/sale",
                        "active": True
                    }
                }
            ]
        }
 
        with self.client.post(
            f"/v{self.api_version}/data/mutate/{self.dataset}",
            json=payload,
            headers=self.headers,
            name="Sanity Mutation - Create Promo Banner",
            catch_response=True
        ) as response:
            if response.status_code not in (200, 201):
                response.failure(f"Create mutation failed: {response.status_code}")
                return
 
            response.success()
 
    @task(1)
    def patch_existing_settings(self):
        payload = {
            "mutations": [
                {
                    "patch": {
                        "id": "siteSettings",
                        "set": {
                            "announcementBar.text": f"Free shipping on orders over ${random.randint(50, 100)}"
                        }
                    }
                }
            ]
        }
 
        with self.client.post(
            f"/v{self.api_version}/data/mutate/{self.dataset}",
            json=payload,
            headers=self.headers,
            name="Sanity Mutation - Patch Site Settings",
            catch_response=True
        ) as response:
            if response.status_code not in (200, 201):
                response.failure(f"Patch mutation failed: {response.status_code}")
                return
 
            response.success()

What this mutation test reveals

This script helps you evaluate:

  • Mutation throughput
  • Write latency under concurrency
  • Error behavior during editorial bursts
  • Stability of content publishing workflows

Be careful with mutation tests. Use staging datasets and clean-up strategies where possible.

Analyzing Your Results

After running Sanity load testing in LoadForge, focus on a few key signals rather than just average response time.

Look at percentile latency

Average response time can hide problems. Instead, examine:

  • p50 for typical performance
  • p95 for user-impacting slowdowns
  • p99 for extreme tail latency

If your Sanity query API shows a p95 of 2000ms while the average is 400ms, users are still experiencing a poor experience under load.

Compare endpoint groups

Because your Locust scripts use named requests, you can compare:

  • Homepage queries
  • Category page queries
  • Product detail queries
  • Preview requests
  • Mutation requests

This helps identify whether the bottleneck is isolated to a specific GROQ query or affects all Sanity traffic.

Watch error codes carefully

During Sanity performance testing, pay attention to:

  • 401 or 403 for token or permission issues
  • 429 for rate limiting
  • 5xx for service instability or upstream issues
  • JSON parsing failures caused by unexpected error payloads

A rising error rate under concurrency is often more important than a modest increase in latency.

Evaluate throughput versus degradation

A strong Sanity setup should maintain acceptable response times as request volume increases. If latency rises sharply after a certain concurrency level, that may indicate:

  • Query complexity issues
  • Dataset or projection inefficiencies
  • Insufficient frontend caching strategy
  • Preview-mode overuse
  • Excessive dereferencing in GROQ

Use LoadForge’s reporting features

LoadForge helps here with:

  • Real-time charts during test execution
  • Distributed load generation from cloud-based infrastructure
  • Global test locations for geographic performance validation
  • Shareable reports for engineering and content teams
  • CI/CD integration for ongoing regression testing

Performance Optimization Tips

If your Sanity stress testing reveals performance issues, these are the first areas to improve.

Simplify GROQ projections

Only request the fields your frontend actually uses. Large projections increase payload size and processing time.

Bad pattern:

  • Fetching full product documents for listing pages

Better pattern:

  • Fetching only title, slug, price, thumbnail, and stock state

Reduce excessive dereferencing

References are powerful, but too many -> expansions in a single query can create expensive reads. Consider flattening frequently accessed content where appropriate.

Split heavy queries when necessary

One giant query is not always better. If a page requests unrelated content blocks, splitting them strategically may improve cacheability and reduce worst-case latency.

Test preview mode separately

Preview traffic often performs differently from public traffic. Measure it independently rather than assuming CDN-backed performance applies to authenticated use.

Optimize image delivery

If your CMS pages depend heavily on Sanity-hosted images:

  • Use appropriately sized image transformations
  • Avoid loading full-size originals
  • Test image-heavy pages separately from JSON API queries

Use realistic wait times and traffic mixes

Good load testing is not just about hammering one endpoint. Use realistic user pacing and endpoint weighting to model actual usage patterns.

Run distributed tests

If your users are global, test from multiple regions. LoadForge’s global test locations help you understand whether latency is due to geography, API behavior, or frontend architecture.

Common Pitfalls to Avoid

Sanity load testing can produce misleading results if you make these common mistakes.

Testing only one simple query

A single lightweight query may look great under load while your real application struggles with more complex category or landing page requests.

Ignoring authentication differences

Public, cached requests are not the same as authenticated preview or private dataset requests. Test both.

Using unrealistic GROQ queries

Your load test should mirror actual application queries. Don’t invent simplified test queries that your frontend never runs.

Overlooking write workloads

If your team imports products, updates inventory content, or publishes campaigns in bursts, mutation testing matters too.

Testing production mutations

Never run destructive or uncontrolled write tests against production datasets unless you have a very deliberate plan.

Not validating response content

A 200 status code does not always mean success. Your Locust scripts should check for expected fields like result or mutation success structures.

Failing to separate CDN and API behavior

If some traffic is served through Sanity’s CDN and some is authenticated directly, test them as distinct scenarios. Mixing them without labeling requests makes analysis harder.

Conclusion

Sanity is a powerful headless CMS for e-commerce and content-driven applications, but like any API-first platform, it needs proper load testing to ensure it performs well under real-world traffic. By testing public content queries, preview workflows, and mutation-heavy editorial scenarios, you can identify slow GROQ queries, authentication bottlenecks, and scaling limits before they affect users or internal teams.

LoadForge makes Sanity performance testing practical with Locust-based scripting, distributed testing, real-time reporting, cloud-based infrastructure, and CI/CD integration. Whether you’re validating a product launch, preparing for a campaign spike, or stress testing your CMS architecture, LoadForge gives you the visibility you need.

Try LoadForge to run your Sanity load testing at scale and make sure your headless CMS is ready for production traffic.

Try LoadForge free for 7 days

Set up your first load test in under 2 minutes. No commitment.