LoadForge LogoLoadForge

Load Testing GraphQL APIs with LoadForge

Load Testing GraphQL APIs with LoadForge

Introduction

GraphQL gives developers a flexible way to query exactly the data they need, but that flexibility can also make performance testing more complicated than with traditional REST APIs. A single GraphQL endpoint can serve dozens of query shapes, deeply nested fields, expensive resolvers, and write-heavy mutations. That means a GraphQL API that looks healthy under light traffic can quickly develop bottlenecks under real-world concurrency.

Load testing GraphQL APIs helps you understand how your schema, resolvers, database access patterns, caching layers, and authentication flows behave under pressure. Whether you are validating a new schema design, preparing for a product launch, or trying to prevent resolver-level slowdowns, performance testing is essential.

In this guide, you’ll learn how to load test GraphQL APIs with LoadForge using realistic Locust scripts. We’ll cover basic queries, authenticated traffic, mutations, and mixed user journeys that better reflect production behavior. Along the way, we’ll look at common GraphQL performance bottlenecks and how to analyze your results using LoadForge’s cloud-based infrastructure, distributed testing, real-time reporting, and global test locations.

Prerequisites

Before you begin load testing your GraphQL API, make sure you have:

  • A GraphQL endpoint, such as:
    • https://api.example.com/graphql
    • https://staging.example.com/graphql
  • At least one valid GraphQL schema with queries and mutations to test
  • Test credentials for authenticated requests
  • A non-production environment when possible, especially for mutation and stress testing
  • Basic familiarity with:
    • GraphQL queries and mutations
    • HTTP headers and bearer token authentication
    • Locust and LoadForge

You should also gather a few key details before writing your test:

  • The most important GraphQL operations in your application
  • Expected traffic patterns
  • Authentication requirements
  • Common query depths and field selections
  • Known expensive resolvers or database-heavy operations
  • Rate limits, query complexity limits, or persisted query settings

For this guide, we’ll assume an e-commerce GraphQL API with operations such as:

  • viewer
  • product
  • searchProducts
  • addToCart
  • checkoutPreview

These are realistic operations that often involve joins, authorization, caching, and database access.

Understanding GraphQL Under Load

GraphQL behaves differently from REST during load testing because many operations are routed through a single endpoint. Instead of testing many URLs, you often test one URL with many different payloads. That creates unique performance testing challenges.

Why GraphQL performance can degrade under load

Common GraphQL bottlenecks include:

  • Resolver waterfalls and N+1 query problems
  • Deeply nested queries that trigger excessive backend calls
  • Expensive field resolvers for computed or aggregated data
  • Poorly cached queries
  • Authentication overhead on every request
  • Mutation contention on shared resources like carts, inventory, or orders
  • Query parsing and validation cost for large dynamic payloads

What to test in a GraphQL API

A strong GraphQL load testing strategy should cover:

  • High-frequency lightweight queries
  • Search queries with filters and pagination
  • Deep nested queries
  • Authenticated user flows
  • Mutations that update state
  • Mixed workloads that combine reads and writes
  • Error handling under concurrency
  • Query complexity and response size behavior

Key GraphQL metrics to watch

When load testing GraphQL APIs, focus on:

  • Response time by operation type
  • P95 and P99 latency for key queries and mutations
  • Requests per second
  • Error rate
  • Timeouts
  • Resolver-heavy query latency
  • Performance differences between shallow and deep queries
  • Infrastructure saturation such as CPU, memory, and database connections

LoadForge makes this easier by letting you run distributed load testing from multiple regions and analyze results in real time, which is especially useful if your GraphQL API is fronted by a CDN, API gateway, or regional edge network.

Writing Your First Load Test

Let’s start with a basic GraphQL query load test. This example sends a product query to a typical GraphQL endpoint and validates the response.

Basic GraphQL query load test

python
from locust import HttpUser, task, between
 
class GraphQLBasicUser(HttpUser):
    wait_time = between(1, 3)
 
    def on_start(self):
        self.headers = {
            "Content-Type": "application/json",
            "Accept": "application/json"
        }
 
    @task
    def get_product_details(self):
        payload = {
            "operationName": "GetProductDetails",
            "query": """
                query GetProductDetails($slug: String!) {
                  product(slug: $slug) {
                    id
                    slug
                    name
                    sku
                    price {
                      amount
                      currency
                    }
                    availability
                    category {
                      id
                      name
                    }
                  }
                }
            """,
            "variables": {
                "slug": "running-shoe-001"
            }
        }
 
        with self.client.post(
            "/graphql",
            json=payload,
            headers=self.headers,
            name="GraphQL Query - GetProductDetails",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure(f"Unexpected status code: {response.status_code}")
                return
 
            data = response.json()
            if "errors" in data:
                response.failure(f"GraphQL errors: {data['errors']}")
                return
 
            product = data.get("data", {}).get("product")
            if not product or product["slug"] != "running-shoe-001":
                response.failure("Product data missing or invalid")
            else:
                response.success()

What this script does

This script simulates a user repeatedly fetching a product record from /graphql. It includes:

  • A realistic GraphQL query with variables
  • JSON request formatting
  • Response validation for both HTTP-level and GraphQL-level errors
  • A friendly request name for easier reporting in LoadForge

Why this matters

Unlike REST, a 200 OK response does not always mean success in GraphQL. GraphQL often returns errors inside the response body, so your load test should inspect the errors field and not rely on HTTP status codes alone.

This first test is useful for:

  • Baseline performance testing
  • Measuring latency for a common read operation
  • Comparing performance before and after schema or resolver changes

Advanced Load Testing Scenarios

Once you have a basic query working, the next step is to simulate production-like behavior. Real users authenticate, search, paginate, add items to carts, and trigger mutations. Below are several more advanced GraphQL load testing examples.

Authenticated GraphQL queries with JWT login

Many GraphQL APIs require authentication for user-specific fields like profile data, orders, and saved payment methods. In this example, users log in through a REST auth endpoint, then use the JWT token for GraphQL queries.

python
from locust import HttpUser, task, between
import random
 
class AuthenticatedGraphQLUser(HttpUser):
    wait_time = between(1, 2)
 
    credentials = [
        {"email": "alice@example.com", "password": "TestPass123!"},
        {"email": "bob@example.com", "password": "TestPass123!"},
        {"email": "carol@example.com", "password": "TestPass123!"}
    ]
 
    def on_start(self):
        user = random.choice(self.credentials)
 
        login_response = self.client.post(
            "/auth/login",
            json={
                "email": user["email"],
                "password": user["password"]
            },
            headers={"Content-Type": "application/json"},
            name="Auth Login"
        )
 
        if login_response.status_code != 200:
            raise Exception(f"Login failed: {login_response.status_code}")
 
        token = login_response.json().get("access_token")
        if not token:
            raise Exception("No access token returned")
 
        self.headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": f"Bearer {token}"
        }
 
    @task(3)
    def get_viewer_profile(self):
        payload = {
            "operationName": "GetViewerProfile",
            "query": """
                query GetViewerProfile {
                  viewer {
                    id
                    email
                    firstName
                    lastName
                    loyaltyPoints
                    defaultShippingAddress {
                      city
                      state
                      postalCode
                      country
                    }
                  }
                }
            """
        }
 
        with self.client.post(
            "/graphql",
            json=payload,
            headers=self.headers,
            name="GraphQL Query - ViewerProfile",
            catch_response=True
        ) as response:
            data = response.json()
            if response.status_code != 200 or "errors" in data:
                response.failure(f"Viewer profile failed: {data}")
            else:
                response.success()
 
    @task(2)
    def get_order_history(self):
        payload = {
            "operationName": "GetOrderHistory",
            "query": """
                query GetOrderHistory($first: Int!) {
                  viewer {
                    orders(first: $first) {
                      edges {
                        node {
                          id
                          orderNumber
                          createdAt
                          total {
                            amount
                            currency
                          }
                          status
                        }
                      }
                    }
                  }
                }
            """,
            "variables": {
                "first": 10
            }
        }
 
        with self.client.post(
            "/graphql",
            json=payload,
            headers=self.headers,
            name="GraphQL Query - OrderHistory",
            catch_response=True
        ) as response:
            data = response.json()
            if response.status_code != 200 or "errors" in data:
                response.failure(f"Order history failed: {data}")
            else:
                response.success()

Why this scenario is important

Authenticated GraphQL traffic is often slower than public queries because it may involve:

  • Token validation
  • Permission checks
  • User-specific resolver logic
  • More cache misses
  • More personalized data assembly

This is exactly the kind of workload you should include in a serious GraphQL performance testing plan.

Search and pagination load testing

Search is one of the most common GraphQL bottlenecks because it often hits databases, search indexes, filters, facets, sorting logic, and pagination. This example simulates users searching products with various terms and pages.

python
from locust import HttpUser, task, between
import random
 
class GraphQLSearchUser(HttpUser):
    wait_time = between(1, 3)
 
    search_terms = ["shoes", "jackets", "backpacks", "water bottle", "wireless earbuds"]
    sort_options = ["RELEVANCE", "PRICE_ASC", "PRICE_DESC", "NEWEST"]
 
    def on_start(self):
        self.headers = {
            "Content-Type": "application/json",
            "Accept": "application/json"
        }
 
    @task
    def search_products(self):
        term = random.choice(self.search_terms)
        sort = random.choice(self.sort_options)
        page = random.randint(1, 5)
 
        payload = {
            "operationName": "SearchProducts",
            "query": """
                query SearchProducts($term: String!, $page: Int!, $pageSize: Int!, $sort: ProductSort!) {
                  searchProducts(term: $term, page: $page, pageSize: $pageSize, sort: $sort) {
                    totalCount
                    pageInfo {
                      currentPage
                      totalPages
                      hasNextPage
                    }
                    items {
                      id
                      name
                      slug
                      brand
                      price {
                        amount
                        currency
                      }
                      thumbnailUrl
                      rating
                    }
                  }
                }
            """,
            "variables": {
                "term": term,
                "page": page,
                "pageSize": 24,
                "sort": sort
            }
        }
 
        with self.client.post(
            "/graphql",
            json=payload,
            headers=self.headers,
            name="GraphQL Query - SearchProducts",
            catch_response=True
        ) as response:
            if response.status_code != 200:
                response.failure(f"HTTP {response.status_code}")
                return
 
            data = response.json()
            if "errors" in data:
                response.failure(f"GraphQL errors: {data['errors']}")
                return
 
            search_results = data.get("data", {}).get("searchProducts", {})
            if "items" not in search_results:
                response.failure("Missing search results")
            else:
                response.success()

What this test reveals

This scenario helps identify:

  • Slow search resolvers
  • Pagination inefficiencies
  • Expensive sort operations
  • Backend pressure from large result sets
  • Cache effectiveness for repeated searches

It also gives you a more realistic picture than simply hammering one static query.

Mixed read/write GraphQL workflow with mutations

GraphQL mutations are especially important to test because they often involve transactions, inventory updates, cart state, and validation logic. This example simulates a user journey where an authenticated customer browses a product, adds it to a cart, and requests a checkout preview.

python
from locust import HttpUser, task, between
import random
 
class GraphQLCartUser(HttpUser):
    wait_time = between(1, 2)
 
    products = [
        {"slug": "running-shoe-001", "quantity": 1},
        {"slug": "gym-bag-204", "quantity": 2},
        {"slug": "water-bottle-550", "quantity": 1}
    ]
 
    def on_start(self):
        login_response = self.client.post(
            "/auth/login",
            json={
                "email": "shopper@example.com",
                "password": "TestPass123!"
            },
            headers={"Content-Type": "application/json"},
            name="Auth Login"
        )
 
        token = login_response.json().get("access_token")
        self.headers = {
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Authorization": f"Bearer {token}"
        }
 
    @task(2)
    def browse_product(self):
        product = random.choice(self.products)
 
        payload = {
            "operationName": "GetProductForCart",
            "query": """
                query GetProductForCart($slug: String!) {
                  product(slug: $slug) {
                    id
                    slug
                    name
                    sku
                    availability
                    price {
                      amount
                      currency
                    }
                  }
                }
            """,
            "variables": {
                "slug": product["slug"]
            }
        }
 
        self.client.post(
            "/graphql",
            json=payload,
            headers=self.headers,
            name="GraphQL Query - ProductForCart"
        )
 
    @task(2)
    def add_to_cart(self):
        product = random.choice(self.products)
 
        payload = {
            "operationName": "AddToCart",
            "query": """
                mutation AddToCart($input: AddToCartInput!) {
                  addToCart(input: $input) {
                    cart {
                      id
                      totalItems
                      subtotal {
                        amount
                        currency
                      }
                      items {
                        id
                        quantity
                        product {
                          id
                          name
                          sku
                        }
                      }
                    }
                    userErrors {
                      field
                      message
                    }
                  }
                }
            """,
            "variables": {
                "input": {
                    "sku": product["slug"],
                    "quantity": product["quantity"]
                }
            }
        }
 
        with self.client.post(
            "/graphql",
            json=payload,
            headers=self.headers,
            name="GraphQL Mutation - AddToCart",
            catch_response=True
        ) as response:
            data = response.json()
            if response.status_code != 200 or "errors" in data:
                response.failure(f"Add to cart failed: {data}")
                return
 
            user_errors = data.get("data", {}).get("addToCart", {}).get("userErrors", [])
            if user_errors:
                response.failure(f"Mutation user errors: {user_errors}")
            else:
                response.success()
 
    @task(1)
    def checkout_preview(self):
        payload = {
            "operationName": "CheckoutPreview",
            "query": """
                query CheckoutPreview {
                  viewer {
                    cart {
                      id
                      totalItems
                      subtotal {
                        amount
                        currency
                      }
                      estimatedTax {
                        amount
                        currency
                      }
                      shippingOptions {
                        id
                        label
                        price {
                          amount
                          currency
                        }
                      }
                      grandTotal {
                        amount
                        currency
                      }
                    }
                  }
                }
            """
        }
 
        self.client.post(
            "/graphql",
            json=payload,
            headers=self.headers,
            name="GraphQL Query - CheckoutPreview"
        )

Why mixed workflows matter

This is much closer to real production traffic than isolated query testing. It helps you evaluate:

  • Mutation latency under concurrency
  • Cart locking or inventory contention
  • Data consistency issues
  • The performance impact of stateful workflows
  • How read and write operations interact under load

In LoadForge, you can scale this across many concurrent users and distributed generators to simulate realistic shopping traffic from multiple regions.

Analyzing Your Results

After running your GraphQL load test, the next step is understanding what the results actually mean.

Focus on operation-level performance

Because GraphQL usually uses one endpoint, the URL alone is not enough for analysis. Name your requests clearly in Locust, such as:

  • GraphQL Query - GetProductDetails
  • GraphQL Query - SearchProducts
  • GraphQL Mutation - AddToCart

This lets LoadForge break out response times by operation, making it much easier to identify problematic queries and mutations.

Pay attention to percentile latency

Average response time can hide serious issues. Look closely at:

  • P50 for normal user experience
  • P95 for degraded experience under load
  • P99 for severe outliers and bottlenecks

If SearchProducts has a reasonable average but a very high P95, you may have intermittent backend slowness, cache misses, or database contention.

Separate HTTP errors from GraphQL errors

A GraphQL API may return:

  • HTTP 200 with GraphQL errors
  • HTTP 429 due to rate limiting
  • HTTP 500 from server exceptions
  • Timeouts from overloaded resolvers or upstream dependencies

Your test scripts should capture both transport-level and application-level failures. This is critical for accurate performance testing.

Compare query shapes

Not all GraphQL operations cost the same. Compare:

  • Shallow vs. nested queries
  • Public vs. authenticated queries
  • Read vs. write operations
  • Cached vs. uncached queries
  • Small vs. large page sizes

These comparisons often reveal where your schema design or resolver implementation needs improvement.

Use LoadForge features for deeper insight

LoadForge is particularly useful for GraphQL load testing because you can:

  • Run distributed load tests from multiple geographic regions
  • Monitor results in real time while a test is running
  • Scale to high concurrency using cloud-based infrastructure
  • Integrate tests into CI/CD pipelines to catch regressions early

For example, if a schema change introduces a slow resolver, you can catch that in staging before it affects production users.

Performance Optimization Tips

Once your GraphQL load testing identifies slow operations, these optimization strategies can help.

Reduce N+1 query problems

Use batching and caching tools like DataLoader to avoid repeated database lookups from nested resolvers.

Limit query complexity

Enforce:

  • Maximum depth
  • Maximum complexity score
  • Field-level cost limits

This helps prevent abusive or accidentally expensive queries from overwhelming your API.

Optimize resolver logic

Review slow resolvers for:

  • Repeated external API calls
  • Inefficient joins
  • Unnecessary field computation
  • Missing indexes
  • Poor pagination implementation

Cache wisely

Consider caching at several levels:

  • Persisted queries
  • Resolver-level caching
  • CDN or edge caching for public queries
  • Search result caching
  • Application object caching

Use pagination consistently

Avoid returning large collections without limits. Cursor-based or page-based pagination helps control response size and backend workload.

Test realistic payloads

Don’t only test tiny queries. Include realistic field selections, nested objects, and pagination sizes so your load testing reflects production behavior.

Common Pitfalls to Avoid

GraphQL performance testing is easy to get wrong if you use unrealistic scripts or overlook GraphQL-specific behavior.

Treating GraphQL like a simple REST endpoint

If you only measure /graphql as one generic URL, you won’t know which operations are slow. Always name requests by operation.

Ignoring GraphQL errors in 200 responses

A 200 OK response can still contain application errors. Always inspect the response body.

Testing only one query

Production traffic is a mix of queries and mutations. A single-operation test won’t reveal real bottlenecks.

Using unrealistic static data

If every virtual user requests the same payload, you may overestimate cache performance and underestimate backend load.

Running mutation tests against production without safeguards

Mutations can create carts, orders, or other side effects. Use isolated test accounts and staging environments whenever possible.

Overlooking authentication overhead

Authenticated traffic often behaves very differently from public traffic. Include login flows or token usage in your tests.

Not correlating API results with backend metrics

Load testing tells you what is slow, but backend monitoring tells you why. Correlate GraphQL latency with database, CPU, memory, and cache metrics.

Conclusion

GraphQL APIs offer flexibility and developer productivity, but they also introduce unique load testing challenges. Queries vary in complexity, mutations can create contention, and a single endpoint can hide multiple performance bottlenecks. By using realistic Locust scripts in LoadForge, you can test the GraphQL operations that matter most, uncover slow resolvers, validate concurrency handling, and improve overall API reliability.

Whether you’re validating product queries, authenticated user flows, search performance, or cart mutations, LoadForge gives you the tools to run scalable load testing with distributed infrastructure, real-time reporting, global test locations, and CI/CD integration.

If you’re ready to load test your GraphQL API with realistic traffic patterns and actionable performance insights, try LoadForge and start building faster, more resilient GraphQL services.

Try LoadForge free for 7 days

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