← All guides

API load testing

Most LoadBolt users are testing APIs. Here's how to do it well — from simple GETs to multi-step authenticated flows.

Why API load testing matters

Your API is the backbone of your app. If it slows down under load, everything downstream — your frontend, your mobile app, your integrations — slows down too. Load testing your API before a launch, a big marketing push, or a Black Friday sale means you find the bottlenecks before your users do.

Simple GET test

The simplest possible API test. Create a test in LoadBolt with your API URL and it auto-generates this for you:

import http from "k6/http";
import { check, sleep } from "k6";

export default function () {
  const res = http.get(`${__ENV.BASE_URL}/api/health`);
  check(res, { "status 200": (r) => r.status === 200 });
  sleep(1);
}

POST with JSON body

For endpoints that accept data:

import http from "k6/http";
import { check, sleep } from "k6";

export default function () {
  const payload = JSON.stringify({
    name: "Load Test Item",
    category: "testing",
  });

  const res = http.post(`${__ENV.BASE_URL}/api/items`, payload, {
    headers: { "Content-Type": "application/json" },
  });

  check(res, {
    "created 201": (r) => r.status === 201,
  });

  sleep(1);
}

Auth header patterns

Two common patterns for authenticated API requests:

import http from "k6/http";

// Bearer token (OAuth, JWT)
export default function () {
  http.get(`${__ENV.BASE_URL}/api/me`, {
    headers: { Authorization: `Bearer ${__ENV.API_TOKEN}` },
  });
}

// API key header
// export default function () {
//   http.get(`${__ENV.BASE_URL}/api/data`, {
//     headers: { "X-API-Key": __ENV.API_KEY },
//   });
// }

Store tokens and keys in LoadBolt's variables UI — they get injected as environment variables at runtime.

Multi-step CRUD flow

A realistic test often walks through a full lifecycle: create a resource, read it, update it, delete it.

import http from "k6/http";
import { check, sleep } from "k6";

const headers = { "Content-Type": "application/json" };

export default function () {
  const base = __ENV.BASE_URL;

  // Create
  const createRes = http.post(
    `${base}/api/items`,
    JSON.stringify({ name: `item-${Date.now()}` }),
    { headers }
  );
  check(createRes, { "created": (r) => r.status === 201 });
  const id = createRes.json("id");

  sleep(0.5);

  // Read
  const getRes = http.get(`${base}/api/items/${id}`);
  check(getRes, { "fetched": (r) => r.status === 200 });

  sleep(0.5);

  // Update
  const putRes = http.put(
    `${base}/api/items/${id}`,
    JSON.stringify({ name: "updated-item" }),
    { headers }
  );
  check(putRes, { "updated": (r) => r.status === 200 });

  sleep(0.5);

  // Delete
  const delRes = http.del(`${base}/api/items/${id}`);
  check(delRes, { "deleted": (r) => r.status === 200 || r.status === 204 });

  sleep(1);
}
This CRUD flow tests your full API lifecycle under load. Each virtual user creates its own resources, so you won't get conflicts between concurrent users.

Testing GraphQL

GraphQL is just POST requests to a single endpoint with a query body:

import http from "k6/http";
import { check, sleep } from "k6";

export default function () {
  const query = JSON.stringify({
    query: `{
      users(first: 10) {
        id
        name
        email
      }
    }`,
  });

  const res = http.post(`${__ENV.BASE_URL}/graphql`, query, {
    headers: { "Content-Type": "application/json" },
  });

  check(res, { "status 200": (r) => r.status === 200 });
  sleep(1);
}

Webhook testing

Testing a webhook endpoint is just a POST with a representative payload:

import http from "k6/http";
import { check, sleep } from "k6";

export default function () {
  const payload = JSON.stringify({
    event: "order.completed",
    data: { orderId: "ord_123", amount: 4999 },
  });

  const res = http.post(`${__ENV.BASE_URL}/api/webhooks/orders`, payload, {
    headers: {
      "Content-Type": "application/json",
      "X-Webhook-Secret": __ENV.WEBHOOK_SECRET,
    },
  });

  check(res, { "accepted": (r) => r.status === 200 });
  sleep(0.5);
}

Environment-specific URLs

Use LoadBolt's variables to keep your scripts reusable across environments. Set BASE_URL to https://staging.yourapp.com for staging tests, then swap to https://api.yourapp.com for production. Same script, different target.

Checking responses

Use check() to validate status codes:

import http from "k6/http";
import { check } from "k6";

export default function () {
  const res = http.get(`${__ENV.BASE_URL}/api/items`);

  check(res, {
    "status is 200": (r) => r.status === 200,
    "response time OK": (r) => r.timings.duration < 500,
  });
}
Important: LoadBolt runs k6 with --discard-response-bodies enabled by default for better performance. This means res.body will be null — status code checks work fine, but you can't inspect the response body. If you need body-based checks, add discardResponseBodies: false to your script's options:
export const options = {
  discardResponseBodies: false,
};
Test your API