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.
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.
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.
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.
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.
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 target | Best for | Writes 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 |
| JSONPlaceholder | quick read-only GETs | β faked | β fixed six resources | β please don't |
| Your staging env | the actual load test β this is the answer for performance numbers that mean anything | β | it IS your API | β its whole job |
| Mockbird | smoke/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.
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.