
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
productionorstaging - 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
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:
https://abc123xy.api.sanity.ioThis 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.
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.
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.
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:
401or403for token or permission issues429for rate limiting5xxfor 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.
LoadForge Team
LoadForge is a load and performance testing platform built on Locust. Our team has been shipping load tests against production systems since 2018, and we write these guides from real customer engagements.
Related guides
Keep going with more guides from the same category.

E-commerce Checkout Load Testing: How to Stress Test Cart and Payment Flows
Test carts, checkout, and payment flows with LoadForge to find bottlenecks and improve conversion during peak traffic.

WooCommerce Load Testing: How to Test Store Performance with LoadForge
Learn how to load test WooCommerce stores with LoadForge to improve product pages, cart speed, and checkout reliability.

BigCommerce Load Testing: How to Performance Test BigCommerce Stores
See how to load test BigCommerce stores with LoadForge to validate storefront speed, API performance, and checkout stability.