GraphQL load testing
GraphQL APIs receive their requests as HTTP POST requests with a JSON body containing the operation and variables. From a load-testing perspective, GraphQL is a specialization of HTTP — you can use the same Taurus native HTTP executor or k6 that you use for REST, with a few GraphQL-specific patterns.
Before you start
Section titled “Before you start”- Read HTTP and REST load testing — GraphQL load testing builds directly 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 errors are 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 supply product IDs, user IDs, or other query variables per VU:
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 key difference is that all operations go through the same endpoint URL (/graphql), so you must use request labels to distinguish operations 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 for
"errors"in the GraphQL response body, not just HTTP status — GraphQL errors are 200 OK at the HTTP level. - Label each operation by its
operationNameso the MaxoPerf per-endpoint breakdown is meaningful. - Use query variables (
$id: ID!) and pass them viavariables:instead of string-interpolating values into the query body — this avoids query-parsing overhead on the server.
Don’t:
- Mix queries and mutations in the same VU loop without understanding the write-side load you are generating — mutations change state and can exhaust data or create unbounded database growth.
- Send introspection queries (
{ __schema { types { name } } }) as part of a load test — disable introspection in production before running load tests against it.
Where to go next
Section titled “Where to go next”- HTTP and REST load testing — the foundational 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.