GraphQL load testing
A GraphQL API receives each request as an HTTP POST with a JSON body that holds the operation and its variables. For load testing, GraphQL is a special case of HTTP. You use the same Taurus native HTTP executor or k6 that you use for REST, plus a few GraphQL-specific patterns.
Before you start
Section titled “Before you start”- Read HTTP and REST load testing. This page builds on the HTTP patterns described there.
- Read Taurus fundamentals or k6 scripts on Maxoperf depending on your engine choice.
GraphQL request structure
Section titled “GraphQL request structure”Every GraphQL operation is a POST request to a single endpoint (typically /graphql) with a JSON body:
{ "query": "query GetProduct($id: ID!) { product(id: $id) { name price stock } }", "variables": { "id": "widget-01" }, "operationName": "GetProduct"}The response is always a 200 OK JSON envelope, even when the operation fails. GraphQL puts errors in the errors field of the response body, not in the HTTP status code.
Taurus native HTTP for GraphQL
Section titled “Taurus native HTTP for GraphQL”execution: - concurrency: 50 ramp-up: 30s hold-for: 5m scenario: graphql-load
scenarios: graphql-load: default-address: https://api.example.com headers: Content-Type: application/json Authorization: "Bearer ${GRAPHQL_TOKEN}"
requests: - label: query-products url: /graphql method: POST body: | { "query": "query ListProducts { products { id name price } }", "operationName": "ListProducts" } assert: - equals: subject: http-code value: '200' - not-contains: subject: body value: '"errors"' # assert no GraphQL errors in response
- label: mutation-create-order url: /graphql method: POST body: | { "query": "mutation CreateOrder($productId: ID!, $qty: Int!) { createOrder(productId: $productId, qty: $qty) { orderId status } }", "variables": {"productId": "widget-01", "qty": 2}, "operationName": "CreateOrder" } assert: - not-contains: subject: body value: '"errors"'Asserting for GraphQL errors
Section titled “Asserting for GraphQL errors”GraphQL always returns HTTP 200, even for errors. Add a not-contains assertion on "errors" in the response body to catch operation failures:
assert: - not-contains: subject: body value: '"errors"'This marks the request as failed in MaxoPerf metrics when the GraphQL server returns an error payload.
k6 for GraphQL
Section titled “k6 for GraphQL”import http from 'k6/http';import { check } from 'k6';
const GQL_URL = 'https://api.example.com/graphql';const TOKEN = __ENV.GRAPHQL_TOKEN; // injected from MaxoPerf secrets
const LIST_PRODUCTS_QUERY = ` query ListProducts { products { id name price } }`;
const CREATE_ORDER_MUTATION = ` mutation CreateOrder($productId: ID!, $qty: Int!) { createOrder(productId: $productId, qty: $qty) { orderId status } }`;
export const options = { vus: 50, duration: '5m',};
function gql(query, variables = {}, operationName = '') { return http.post( GQL_URL, JSON.stringify({ query, variables, operationName }), { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${TOKEN}`, }, } );}
export default function () { // Query const listRes = gql(LIST_PRODUCTS_QUERY, {}, 'ListProducts'); check(listRes, { 'list products status 200': (r) => r.status === 200, 'no graphql errors': (r) => !r.json('errors'), });
// Mutation const createRes = gql(CREATE_ORDER_MUTATION, { productId: 'widget-01', qty: 2 }, 'CreateOrder'); check(createRes, { 'create order status 200': (r) => r.status === 200, 'order id present': (r) => r.json('data.createOrder.orderId') !== null, 'no graphql errors': (r) => !r.json('errors'), });}Parameterizing queries with variables
Section titled “Parameterizing queries with variables”For data-driven GraphQL tests, use a CSV data entity in MaxoPerf to give each VU its own product IDs, user IDs, or other query variables:
scenarios: parameterized-graphql: data-sources: - path: product-ids.csv # uploaded as a Test asset; first column = productId variable-names: productId
requests: - label: query-product url: /graphql method: POST body: | { "query": "query GetProduct($id: ID!) { product(id: $id) { name price } }", "variables": {"id": "${productId}"}, "operationName": "GetProduct" }See Cookbook: CSV data-driven test for the full CSV setup workflow.
Reading GraphQL results in MaxoPerf
Section titled “Reading GraphQL results in MaxoPerf”GraphQL results appear in the same MaxoPerf run-detail view as REST results. The difference is that all operations go through the same endpoint URL (/graphql). Use request labels to tell operations apart in the per-endpoint breakdown:
- In Taurus YAML: use the
label:key on each request. - In k6: pass a
nametag in the request params:http.post(url, body, { tags: { name: 'ListProducts' } }).
Without labels, all GraphQL operations appear as a single row in the per-endpoint panel.
Do / don’t
Section titled “Do / don’t”Do:
- Assert on
"errors"in the GraphQL response body as well as the HTTP status. GraphQL errors are 200 OK at the HTTP level. - Label each operation by its
operationNameso the MaxoPerf per-endpoint breakdown shows one row per operation. - Use query variables (
$id: ID!) and pass them invariables:instead of interpolating values into the query string. This avoids query-parsing overhead on the server.
Don’t:
- Mix queries and mutations in the same VU loop without knowing the write load you generate. Mutations change state and can exhaust data or grow the database without bound.
- Send introspection queries (
{ __schema { types { name } } }) as part of a load test. Disable introspection in production before you load-test it.
Where to go next
Section titled “Where to go next”- HTTP and REST load testing: the HTTP patterns this page builds on.
- Cookbook: CSV data-driven test: parameterize queries with a CSV variable list.
- Cookbook: correlation / dynamic values: extract values from query responses and use them in later mutations.
- gRPC load testing: next protocol page.