← All guides

A target API for your k6 scripts (test.k6.io is gone)

Grafana k6 is the best thing that ever happened to load testing β€” real scripts in JavaScript, a proper checks/thresholds model, and a CLI you can wire into any pipeline. But every k6 script needs a URL to hit, and in 2025 Grafana retired the URLs a hundred tutorials were built on: test.k6.io, test-api.k6.io and httpbin.test.k6.io all 302-redirect to quickpizza.grafana.com now (we curled all three before publishing). The famous crocodiles API is gone. If you're following an older course, your script is quietly testing a pizza website.

The insidious part β€” this is the canonical old tutorial script, run with k6 v2.2.0 today:

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

export default function () {
  const res = http.get('https://test-api.k6.io/public/crocodiles/');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'got crocodiles': (r) => r.json().length > 0,
  });
}
βœ“ status is 200          ← k6 follows the redirect; the pizza site returns 200
βœ— got crocodiles
  ↳  0% β€” βœ“ 0 / βœ— 1      ← r.json() on an HTML page β†’ undefined

A script that only checks status codes still passes β€” against the wrong website. QuickPizza itself is a good demo app (more on it below, including when you should use it instead of us) β€” but it's a fixed app with fixed endpoints. You can't make it look like your API.

Mockbird is a free hosted mock-API service: you describe resources (or import your OpenAPI spec) and get a live REST+GraphQL backend with realistic seeded data at a stable URL. That makes it a good target for the functional side of k6 β€” smoke tests, checks, CI gates, resilience drills. Here's the receipts-first tour; every script below was run against production before publishing and the outputs shown are real.

1. A smoke test that passes in 60 seconds

No signup, no target app to deploy β€” the shared demo project is live right now:

// smoke.js β€” run with: k6 run smoke.js
import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE = 'https://mockbird.mockbird.workers.dev/m/demo';

export const options = {
  vus: 2,
  iterations: 10,
  thresholds: {
    http_req_duration: ['p(95)<800'],
    checks: ['rate==1.0'],
  },
};

export default function () {
  const res = http.get(`${BASE}/products?_page=1&_limit=5`);
  check(res, {
    'status is 200': (r) => r.status === 200,
    'returns 5 products': (r) => r.json().length === 5,
    'total count header': (r) => r.headers['X-Total-Count'] === '30',
    'records have real fields': (r) => typeof r.json()[0].price === 'number',
  });
  sleep(1);
}
β–ˆ THRESHOLDS
  checks             βœ“ 'rate==1.0' rate=100.00%
  http_req_duration  βœ“ 'p(95)<800' p(95)=168.04ms

βœ“ status is 200
βœ“ returns 5 products
βœ“ total count header
βœ“ records have real fields

Note the header check: pagination here is real (X-Total-Count, _page/_limit, json-server conventions), so your checks exercise the same response shapes your client code parses in production.

2. Writes that actually persist (the tutorial APIs fake this)

JSONPlaceholder β€” the other URL every k6 tutorial reaches for β€” accepts your POST and throws it away: you get {"id": 101} back, and GET on that id 404s. So the most important k6 pattern of all, write-then-verify, can't be practiced there. Here it can:

// writes.js β€” full CRUD round-trip, every step verifiable
import http from 'k6/http';
import { check } from 'k6';

const BASE = 'https://mockbird.mockbird.workers.dev/m/demo';
const JSON_H = { headers: { 'Content-Type': 'application/json' } };

export const options = { vus: 1, iterations: 1 };

export default function () {
  const created = http.post(`${BASE}/products`,
    JSON.stringify({ name: 'k6 test product', price: 9.99, category: 'tools', inStock: true }),
    JSON_H);
  check(created, { 'POST β†’ 201': (r) => r.status === 201 });
  const id = created.json().id;

  // the write actually persisted β€” read it back
  const read = http.get(`${BASE}/products/${id}`);
  check(read, {
    'GET it back β†’ 200': (r) => r.status === 200,
    'same record': (r) => r.json().name === 'k6 test product',
  });

  const patched = http.patch(`${BASE}/products/${id}`, JSON.stringify({ price: 19.99 }), JSON_H);
  check(patched, { 'PATCH β†’ 200, new price': (r) => r.json().price === 19.99 });
  const del = http.del(`${BASE}/products/${id}`);
  check(del, { 'DELETE β†’ 200': (r) => r.status === 200 });
}
βœ“ POST β†’ 201
βœ“ GET it back β†’ 200
βœ“ same record
βœ“ PATCH β†’ 200, new price
βœ“ DELETE β†’ 200

Parallel VUs write safely, too β€” we found (and fixed) a record-id race in our own write path using k6 while writing this guide: 5 VUs POSTing to one resource concurrently is exactly the kind of client no one's curl testing catches. 25/25 concurrent creates now come back 201.

3. An ephemeral backend per test run β€” setup() / teardown()

The shared demo is fine for smoke checks, but its data belongs to everyone. k6's lifecycle hooks map perfectly onto Mockbird's one-call project create β€” each run gets a fresh, isolated, seeded backend, and it disappears afterwards:

// ephemeral.js β€” fresh isolated backend per run, no state bleed between runs
import http from 'k6/http';
import { check } from 'k6';

const HOST = 'https://mockbird.mockbird.workers.dev';
const JSON_H = { headers: { 'Content-Type': 'application/json' } };

export const options = { vus: 3, iterations: 12 };

export function setup() {
  const res = http.post(`${HOST}/api/projects`,
    JSON.stringify({ name: 'k6-run', preset: 'ecommerce' }), JSON_H);
  if (res.status !== 201 && res.status !== 200) {
    throw new Error(`project create failed: ${res.status} ${res.body}`);
  }
  const p = res.json();
  return { base: p.baseUrl, id: p.id, adminKey: p.adminKey };
}

export default function (data) {
  const created = http.post(`${data.base}/orders`,
    JSON.stringify({ status: 'pending', total: 42.5 }), JSON_H);
  check(created, { 'order created': (r) => r.status === 201 });

  const list = http.get(`${data.base}/orders?status=pending&_limit=100`);
  check(list, {
    'listed with filter': (r) => r.status === 200,
    'my write is in there': (r) => r.json().some((o) => o.id === created.json().id),
  });
}

export function teardown(data) {
  const res = http.del(`${HOST}/api/projects/${data.id}`, null,
    { headers: { 'x-admin-key': data.adminKey } });
  check(res, { 'project deleted': (r) => r.status === 200 || r.status === 204 });
}
checks_succeeded...: 100.00% 37 out of 37
βœ“ order created
βœ“ listed with filter
βœ“ my write is in there
βœ“ project deleted

This is the same ephemeral-project pattern we use in GitHub Actions. Want the backend to match your real API instead of a preset? setup() can pipe your OpenAPI spec to /api/projects/import and you're testing against your own schema.

4. Resilience drills: forced 503s, chaos, latency budgets

k6 is where people discover their client has no timeout and their retry loop retries nothing. Every Mockbird endpoint takes simulation query params, so the failure cases are one query string away β€” no proxy, no middleware:

// drills.js
import http from 'k6/http';
import { check } from 'k6';

const BASE = 'https://mockbird.mockbird.workers.dev/m/demo';

// tell k6 the drill's 503 is what we WANT (keeps http_req_failed clean)
http.setResponseCallback(http.expectedStatuses(200, 201, 503));

export const options = { vus: 1, iterations: 3 };

export default function () {
  // 1) forced 503 β€” your error handling, on demand
  const down = http.get(`${BASE}/products?mock_status=503`);
  check(down, { 'drill: got the 503': (r) => r.status === 503 });

  // 2) chaos: ~50% of requests fail with a random 5xx/429 β€” retry-loop drill
  let ok = null;
  for (let attempts = 0; attempts < 5; attempts++) {
    const r = http.get(`${BASE}/products?mock_chaos=0.5&_limit=1`);
    if (r.status === 200) { ok = r; break; }
  }
  check(ok, { 'chaos: retry loop eventually wins': (r) => r !== null && r.status === 200 });

  // 3) latency budget: server takes 2s, client budget is 1s β†’ timeout path exercised
  const slow = http.get(`${BASE}/products?mock_delay=2000&_limit=1`, { timeout: '1s' });
  check(slow, { 'timeout: request aborted client-side': (r) => r.status === 0 && r.error_code !== 0 });
}
βœ“ drill: got the 503
βœ“ chaos: retry loop eventually wins
βœ“ timeout: request aborted client-side

Notes from running this for real: chaos-failed responses carry an x-mockbird-chaos: injected header so you can tell drill failures from real ones; chaos-failed writes are not applied (server-died-before-processing semantics β€” safe to aim retry logic at); and without the setResponseCallback line your http_req_failed metric counts every intentional 503 and timeout, which makes dashboards lie. There's also mock_seq for exact deterministic sequences (fail, fail, succeed) β€” see testing loading and error states.

5. The honest section: do NOT load test us

k6's headline feature is generating serious load. Please don't point that at Mockbird. Projects have a 10,000 requests/day cap (the shared demo: 50,000) β€” a smoke suite or CI gate barely dents it; vus: 100, duration: '10m' blows through it in seconds and you'll get 429s, by design. We're the functional target, not the load target. Where to aim what:

k6 targetBest forWrites persist?Shapeable to your API?Real load OK?
QuickPizza (hosted)learning k6 basics, following current Grafana docs✘ (shared env; they ask you to avoid high load too)✘ fixed demo app✘ shared
QuickPizza (self-hosted)real load practice with Grafana observability β€” it's open source and excellent for thisβœ” your instance✘ fixed demo appβœ” your hardware
JSONPlaceholderquick read-only GETs✘ faked✘ fixed six resources✘ please don't
Your staging envthe actual load test β€” this is the answer for performance numbers that mean anythingβœ”it IS your APIβœ” its whole job
Mockbirdsmoke/functional/CI checks, resilience drills, write-then-verify, backend-doesn't-exist-yetβœ” real CRUDβœ” your schema, or OpenAPI import✘ 10k/day cap

Performance numbers measured against any mock are fiction β€” you'd be measuring our Cloudflare edge, not your backend. Use us to prove your client's behavior (checks, retries, timeouts, parsing), then aim the load at infrastructure you own.

Try it now

curl -s "https://mockbird.mockbird.workers.dev/m/demo/products?_limit=3"

…then paste smoke.js from section 1 and k6 run smoke.js. Or create your own project in one click β€” free, no signup required. Full docs Β· machine-readable API index.

Related: the ephemeral-project pattern in GitHub Actions, the same ideas in Tavern and Karate, testing loading and error states, rate-limit simulation, and deterministic test data with snapshots.

Verification: every script on this page was run verbatim with k6 v2.2.0 (fresh binary from GitHub releases, 5 Sep 2026) against production before publishing; outputs shown are excerpts from real runs, including the failing crocodiles check. The redirects were confirmed with curl the same day: test.k6.io, test-api.k6.io and test-api.k6.io/public/crocodiles/ all 302 to quickpizza.grafana.com. If a snippet here doesn't work, that's a bug: tell us.