
Introduction
Headless CMS platforms power modern websites, mobile apps, digital storefronts, kiosks, and omnichannel experiences by exposing content through APIs instead of rendering pages directly. Whether you use Contentful-style delivery APIs, Strapi, Sanity, Directus, Contentstack, or a custom headless CMS, your application depends on fast and reliable content delivery under load.
If your content API slows down during a product launch, holiday sale, or marketing campaign, the impact can be immediate: slow page loads, missing product descriptions, delayed article rendering, and frustrated users. That is why load testing headless CMS infrastructure is essential. With proper performance testing and stress testing, you can validate how your content APIs behave when traffic spikes, identify bottlenecks in caching and query execution, and ensure your CMS scales as expected.
In this guide, you’ll learn how to load test headless CMS platforms with LoadForge using realistic Locust scripts. We’ll cover common API patterns such as public content delivery, authenticated preview access, filtered collection queries, localization, and media-heavy content requests. You’ll also see how LoadForge’s distributed testing, real-time reporting, cloud-based infrastructure, CI/CD integration, and global test locations can help you simulate real-world traffic patterns for content APIs.
Prerequisites
Before you begin load testing a headless CMS, make sure you have:
- A LoadForge account
- Access to your headless CMS API endpoints
- API credentials such as:
- Delivery API token
- Preview API token
- Bearer token or personal access token
- Optional OAuth or JWT-based authentication credentials
- Knowledge of your content model, including:
- Collection names
- Entry types
- Slugs
- Locales
- Query parameters for filters, pagination, and includes
- A test environment or staging CMS instance that mirrors production behavior
- Sample content already populated in the CMS
- Expected traffic patterns, such as:
- Homepage content fetches
- Product listing content
- Article detail requests
- Preview/editor traffic
- Search/filter API calls
You should also know whether your CMS sits behind a CDN, API gateway, reverse proxy, or edge cache. This matters because performance testing a cached delivery API produces very different results than testing origin-only content retrieval.
Understanding Headless CMS Under Load
A headless CMS typically serves JSON content through REST or GraphQL APIs. Under load, performance depends on more than just the CMS application itself. You are often testing a chain of systems:
- CDN or edge cache
- API gateway
- Authentication layer
- CMS application server
- Search/indexing service
- Database
- Object storage for assets
- Third-party plugins or webhooks
Common headless CMS bottlenecks
When running load testing or stress testing against a headless CMS, these are the most common bottlenecks:
Cache misses on popular content
Public content delivery often performs well when cached, but cache misses can create sudden latency spikes. If your homepage or product detail content is frequently invalidated, the CMS may need to rebuild responses from the database repeatedly.
Deep relational queries
Many headless CMS APIs support nested content relationships such as:
- Homepage → featured categories → products → media assets
- Blog post → author → related posts → category metadata
These deep includes can dramatically increase response time under concurrent load.
Localization and variant expansion
Localized content often requires additional filtering and fallback logic. A single request for /api/articles?locale=fr-FR may trigger more expensive lookups than the default locale.
Preview and draft access
Preview endpoints usually bypass cache and query draft content directly. Editorial teams may not generate huge traffic, but preview APIs can be much more expensive per request.
Asset-heavy responses
Media-rich content APIs can become slow when entries include many images, videos, or transformed asset URLs. Even if the JSON payload is small, the content generation process may be expensive.
Pagination and filtering
Product listing pages, category pages, and content archives often rely on filters, sorting, and pagination. These endpoints can create database pressure, especially if indexes are missing.
Rate limiting and auth validation
Authenticated CMS APIs often validate tokens on every request. Under load, auth middleware or external identity providers can become a bottleneck before the CMS itself does.
For realistic performance testing, your scripts should reflect these usage patterns instead of hitting a single endpoint repeatedly.
Writing Your First Load Test
Let’s start with a basic headless CMS load test that simulates public content delivery. This example assumes a CMS exposing REST endpoints similar to Strapi or Directus, with bearer token authentication for content delivery.
This script tests three common read operations:
- Homepage content
- Product listing content
- Article detail content
from locust import HttpUser, task, between
class HeadlessCMSPublicAPIUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
self.headers = {
"Authorization": "Bearer YOUR_DELIVERY_API_TOKEN",
"Accept": "application/json"
}
@task(5)
def get_homepage_content(self):
self.client.get(
"/api/pages?filters[slug][$eq]=home&populate[hero][populate]=image&populate[featured_products][populate]=thumbnail",
headers=self.headers,
name="GET /api/pages home"
)
@task(3)
def get_product_listing_content(self):
self.client.get(
"/api/products?filters[status][$eq]=published&pagination[page]=1&pagination[pageSize]=24&sort[0]=publishedAt:desc&populate=primaryImage",
headers=self.headers,
name="GET /api/products listing"
)
@task(2)
def get_article_detail(self):
self.client.get(
"/api/articles?filters[slug][$eq]=spring-fashion-trends-2026&populate[author]=*&populate[coverImage]=*&populate[related_articles]=*",
headers=self.headers,
name="GET /api/articles detail"
)What this script does
This first test simulates a public content consumer, such as a storefront frontend or static site server requesting published content. It uses weighted tasks to reflect typical traffic:
- Homepage requests are most frequent
- Product listing pages are common
- Article detail pages occur less often
Why this is realistic
A headless CMS rarely serves only one type of content. Real traffic mixes homepage, listing, and detail requests, and each may have different query complexity. By giving tasks different weights, you get more representative load testing results.
What to look for in LoadForge
When you run this script in LoadForge, pay attention to:
- Median and p95 response times by endpoint
- Error rates during ramp-up
- Whether listing or detail endpoints degrade faster than homepage content
- Differences between cached and uncached behavior
With LoadForge, you can scale this test across multiple regions to see how global traffic affects your content APIs, especially if your CMS serves international storefronts or editorial properties.
Advanced Load Testing Scenarios
Basic read testing is a good start, but most headless CMS performance testing needs to include more realistic workflows. Below are several advanced scenarios that reflect how modern content APIs are actually used.
Authenticated preview and draft content testing
Preview APIs are critical for editorial teams and content reviewers. These endpoints often bypass CDN caching and can place significant load on the CMS backend.
This example simulates an editor logging in and requesting draft content previews.
from locust import HttpUser, task, between
class HeadlessCMSPreviewUser(HttpUser):
wait_time = between(2, 5)
def on_start(self):
login_payload = {
"identifier": "editor@example.com",
"password": "SuperSecurePassword123!"
}
response = self.client.post(
"/api/auth/local",
json=login_payload,
headers={"Accept": "application/json"},
name="POST /api/auth/local"
)
if response.status_code == 200:
token = response.json().get("jwt")
self.headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
else:
self.headers = {"Accept": "application/json"}
@task(4)
def preview_landing_page(self):
self.client.get(
"/api/pages?filters[slug][$eq]=summer-sale&publicationState=preview&populate[sections][populate]=*&locale=en-US",
headers=self.headers,
name="GET preview landing page"
)
@task(3)
def preview_product_content(self):
self.client.get(
"/api/products?filters[sku][$eq]=SKU-98421&publicationState=preview&populate[gallery]=*&populate[seo]=*",
headers=self.headers,
name="GET preview product"
)
@task(2)
def preview_blog_post(self):
self.client.get(
"/api/articles?filters[slug][$eq]=editors-picks-april&publicationState=preview&populate[content_blocks]=*&populate[author]=*",
headers=self.headers,
name="GET preview blog post"
)Why preview testing matters
Preview requests are usually more expensive because they:
- Bypass caching
- Access draft content
- Fetch deeply nested content relationships
- Trigger permission checks
If your editors complain that preview is slow during busy publishing windows, this type of load test can reveal whether the issue is auth, database performance, or query complexity.
Collection filtering, pagination, and localization
E-commerce and CMS workloads often combine content with locale-specific product or campaign data. This scenario simulates category browsing, localized article retrieval, and filtered campaign content.
from locust import HttpUser, task, between
import random
class LocalizedContentAPIUser(HttpUser):
wait_time = between(1, 2)
def on_start(self):
self.headers = {
"Authorization": "Bearer YOUR_DELIVERY_API_TOKEN",
"Accept": "application/json"
}
self.locales = ["en-US", "fr-FR", "de-DE"]
self.categories = ["mens-shoes", "womens-jackets", "accessories", "new-arrivals"]
@task(4)
def browse_category_content(self):
locale = random.choice(self.locales)
category = random.choice(self.categories)
self.client.get(
f"/api/category-pages?filters[slug][$eq]={category}&locale={locale}&populate[seo]=*&populate[banner][populate]=image&populate[featured_products][populate]=primaryImage",
headers=self.headers,
name="GET localized category page"
)
@task(3)
def list_campaign_entries(self):
locale = random.choice(self.locales)
self.client.get(
f"/api/campaigns?filters[active][$eq]=true&filters[channel][$eq]=web&locale={locale}&pagination[page]=1&pagination[pageSize]=12&sort[0]=priority:asc",
headers=self.headers,
name="GET active campaigns"
)
@task(2)
def localized_articles(self):
locale = random.choice(self.locales)
self.client.get(
f"/api/articles?locale={locale}&filters[category][slug][$eq]=style-guides&pagination[page]=1&pagination[pageSize]=10&populate[coverImage]=*&sort[0]=publishedAt:desc",
headers=self.headers,
name="GET localized articles"
)
@task(1)
def search_content_blocks(self):
locale = random.choice(self.locales)
self.client.get(
f"/api/content-blocks?locale={locale}&filters[type][$eq]=promo_banner&filters[enabled][$eq]=true",
headers=self.headers,
name="GET promo content blocks"
)Why this scenario is important
This test models common frontend behavior where users browse localized content across different sections. It is especially useful for performance testing headless CMS setups used in international e-commerce.
It can uncover:
- Slow locale-based queries
- Poor indexing on filters and sorting fields
- Pagination inefficiencies
- Problems with relational population in category and campaign data
GraphQL content API load testing
Many headless CMS platforms expose GraphQL endpoints for frontend applications. GraphQL adds flexibility, but poorly designed queries can create heavy backend load.
This example simulates a storefront requesting homepage content, featured products, and editorial articles through GraphQL.
from locust import HttpUser, task, between
import random
class HeadlessCMSGraphQLUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
self.headers = {
"Authorization": "Bearer YOUR_GRAPHQL_API_TOKEN",
"Content-Type": "application/json",
"Accept": "application/json"
}
self.locales = ["en-US", "fr-FR"]
@task(5)
def homepage_query(self):
locale = random.choice(self.locales)
query = {
"query": """
query HomepageContent($locale: I18NLocaleCode!) {
pages(filters: { slug: { eq: "home" } }, locale: $locale) {
data {
attributes {
title
hero {
headline
subheadline
image {
data {
attributes {
url
alternativeText
}
}
}
}
featured_products {
data {
attributes {
name
slug
price
primaryImage {
data {
attributes {
url
}
}
}
}
}
}
}
}
}
}
""",
"variables": {
"locale": locale
}
}
self.client.post(
"/graphql",
json=query,
headers=self.headers,
name="POST /graphql homepage"
)
@task(3)
def product_collection_query(self):
locale = random.choice(self.locales)
query = {
"query": """
query ProductCollection($locale: I18NLocaleCode!) {
products(
filters: { status: { eq: "published" } }
sort: "publishedAt:desc"
pagination: { page: 1, pageSize: 24 }
locale: $locale
) {
data {
attributes {
name
slug
sku
price
primaryImage {
data {
attributes {
url
}
}
}
}
}
}
}
""",
"variables": {
"locale": locale
}
}
self.client.post(
"/graphql",
json=query,
headers=self.headers,
name="POST /graphql product collection"
)
@task(2)
def editorial_article_query(self):
query = {
"query": """
query ArticleDetail {
articles(filters: { slug: { eq: "spring-fashion-trends-2026" } }) {
data {
attributes {
title
excerpt
content
author {
data {
attributes {
name
}
}
}
coverImage {
data {
attributes {
url
}
}
}
}
}
}
}
"""
}
self.client.post(
"/graphql",
json=query,
headers=self.headers,
name="POST /graphql article detail"
)Why GraphQL needs careful load testing
GraphQL can hide expensive backend operations behind a single endpoint. During stress testing, watch for:
- Increased latency on complex nested queries
- High CPU usage on the CMS origin
- N+1 query behavior
- Payload size growth from overfetching
A GraphQL API may appear simple because every request hits /graphql, but LoadForge’s detailed reporting can still help you break down performance by named request and query type.
Analyzing Your Results
Once your headless CMS load testing scenario is running, the next step is interpreting the results correctly.
Key metrics to focus on
Response time percentiles
Average response time is useful, but p95 and p99 are more important. If most requests are fast but 5% are very slow, users will still notice performance problems.
For headless CMS APIs, a useful rule of thumb is:
- Public cached content should be consistently fast
- Preview and uncached content can be slower, but should remain stable under expected concurrency
- Listing and filtered collection endpoints should not degrade sharply during ramp-up
Requests per second
This tells you how much traffic your CMS can sustain. Compare this against expected production peaks, such as:
- Product launches
- Flash sales
- Campaign launches
- News or editorial spikes
- Holiday traffic
Error rate
Look for:
- 401 or 403 errors from auth failures
- 429 errors from rate limiting
- 500 errors from CMS application failures
- 502 or 504 errors from reverse proxies or gateways
A rising error rate under load usually indicates an upstream bottleneck or insufficient scaling.
Endpoint-specific degradation
Not all endpoints behave equally. In headless CMS performance testing, slowdowns often appear first in:
- Deeply populated relational endpoints
- Preview endpoints
- GraphQL queries with nested fields
- Localized collection queries
- Large page-size listing requests
Cached vs uncached behavior
If your public API sits behind a CDN, compare:
- Warm-cache performance
- Cold-cache performance
- Cache-hit ratio during test execution
A CMS that performs well only when fully cached may still struggle during content invalidation events.
Using LoadForge effectively
LoadForge makes this easier with:
- Real-time reporting to detect latency spikes as tests run
- Distributed testing to simulate traffic from multiple regions
- Cloud-based infrastructure so you can generate large-scale load without managing your own runners
- CI/CD integration to run performance testing automatically before releases
For headless CMS teams, this is especially valuable when validating content delivery before major launches.
Performance Optimization Tips
After load testing your headless CMS, you’ll often uncover opportunities for improvement. Here are some of the most effective optimization strategies.
Reduce query depth
Avoid overusing populate=* or deeply nested GraphQL queries unless absolutely necessary. Fetch only the fields required for the frontend view.
Improve caching strategy
Use CDN and edge caching for public content where possible. Separate preview traffic from public traffic so draft access does not interfere with delivery performance.
Add indexes for filters and sorting
If category, slug, locale, published status, or date filters are slow, check database indexing. Listing and archive endpoints often benefit the most.
Optimize pagination
Large page sizes can create expensive queries and oversized JSON responses. Test realistic page sizes such as 12, 24, or 48 instead of requesting hundreds of entries at once.
Precompute expensive content
If homepage or campaign content requires assembling many related objects, consider precomputing or caching the final payload.
Tune GraphQL resolvers
Use batching and resolver optimization to prevent N+1 query issues. GraphQL is powerful, but poorly implemented resolvers can collapse under load.
Separate editorial and delivery workloads
Preview, publishing, and content management operations should not compete with public delivery traffic whenever possible.
Monitor payload size
Large JSON responses increase transfer time and parsing overhead. Remove unused fields and unnecessary nested relationships.
Common Pitfalls to Avoid
Headless CMS load testing can produce misleading results if the test design is unrealistic. Avoid these common mistakes.
Testing only one endpoint
A single URL hit repeatedly does not reflect real CMS usage. Use a mix of homepage, listing, detail, localized, and preview requests.
Ignoring authentication flows
If your real application uses bearer tokens, JWT login, or preview auth, include that in your Locust scripts. Otherwise, your performance testing won’t represent production behavior.
Overlooking cache behavior
Testing only cached responses can hide backend problems. Testing only uncached responses can exaggerate issues. You need both perspectives.
Using synthetic queries that no frontend actually sends
Your scripts should mirror real application requests, including realistic filters, slugs, locales, and includes.
Not weighting traffic patterns
Homepage traffic is usually more common than article detail or preview traffic. Weighted tasks help your load testing model actual usage.
Forgetting about global audiences
If your content is consumed across multiple countries, use LoadForge’s global test locations to simulate geographic distribution and spot latency differences.
Testing production without safeguards
Stress testing a live CMS can affect real users and editors. Prefer staging environments or carefully controlled production tests with clear limits.
Conclusion
Headless CMS platforms are the foundation of modern digital experiences, but they must be able to deliver content quickly and reliably under real-world traffic. By load testing public delivery APIs, preview endpoints, localized content queries, and GraphQL operations, you can uncover bottlenecks before they affect customers, editors, or storefront performance.
With LoadForge, you can run realistic headless CMS performance testing at scale using Locust-based Python scripts, distributed cloud infrastructure, real-time reporting, and CI/CD-friendly workflows. Whether you’re validating a new content architecture, preparing for a campaign launch, or stress testing your CMS before peak e-commerce traffic, LoadForge gives you the tools to test with confidence.
Try LoadForge to build and run your headless CMS load tests, and make sure your content APIs are ready for whatever traffic comes next.
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.

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.

Drupal Load Testing: How to Stress Test Drupal Sites with LoadForge
Find out how to load test Drupal sites with LoadForge to detect performance issues and maintain speed during heavy traffic.

WordPress Load Testing: How to Stress Test WordPress Sites with LoadForge
Discover how to load test WordPress sites with LoadForge to measure performance, find slow plugins, and prepare for traffic spikes.