setup() and teardown() are K6's hooks for one-time work that should not run inside the test loop. Getting this right separates tests that produce accurate results from tests that accidentally measure their own setup overhead or hammer endpoints they were not supposed to.
What setup() does
setup() runs exactly once before any virtual user starts. Whatever it returns is serialised to JSON by K6 and passed — as a single data object — to every call of export default function and to teardown().
The most common use case is authentication:
import http from 'k6/http';
import { check } from 'k6';
export function setup() {
const res = http.post('https://api.example.com/auth/login', JSON.stringify({
email: 'loadtest@example.com',
password: 'TestPass@1234',
}), {
headers: { 'Content-Type': 'application/json' },
});
check(res, { 'login succeeded': (r) => r.status === 200 });
return {
token: res.json('token'),
userId: res.json('data.userId'),
};
}
export default function (data) {
const headers = { Authorization: `Bearer ${data.token}` };
const res = http.get(`https://api.example.com/profile/${data.userId}`, { headers });
check(res, { 'profile loaded': (r) => r.status === 200 });
}setup() logs in once. All VUs — whether five or five hundred — receive the same token and userId through data. None of them log in themselves. The auth endpoint is hit exactly once, not once per VU per iteration.
The data flow
Creating and cleaning up test data
Authentication is the simplest use case. A more complete pattern creates test data in setup and deletes it in teardown, so the test never leaves traces in the database:
import http from 'k6/http';
import { check } from 'k6';
const BASE = 'https://api.example.com';
export const options = { vus: 10, duration: '1m' };
export function setup() {
// Get an admin token
const adminLogin = http.post(`${BASE}/auth/login`, JSON.stringify({
email: 'admin@example.com',
password: 'AdminPass@1234',
}), { headers: { 'Content-Type': 'application/json' } });
const adminToken = adminLogin.json('token');
const authHeader = { Authorization: `Bearer ${adminToken}` };
// Create a dedicated test user for this load test run
const newUser = http.post(
`${BASE}/admin/users`,
JSON.stringify({ email: 'k6-loadtest@example.com', role: 'viewer' }),
{ headers: { ...authHeader, 'Content-Type': 'application/json' } }
);
check(newUser, { 'test user created': (r) => r.status === 201 });
return {
adminToken,
testUserId: newUser.json('id'),
};
}
export default function (data) {
const headers = { Authorization: `Bearer ${data.adminToken}` };
const res = http.get(`${BASE}/users/${data.testUserId}`, { headers });
check(res, { 'user fetched': (r) => r.status === 200 });
}
export function teardown(data) {
// Delete the test user created in setup
const headers = { Authorization: `Bearer ${data.adminToken}` };
const res = http.del(`${BASE}/admin/users/${data.testUserId}`, null, { headers });
check(res, { 'test user deleted': (r) => r.status === 204 });
}The test run leaves the database in the same state it was in before. This is essential when running load tests against shared staging environments.
Timeouts and failure handling
By default, K6 gives setup() 60 seconds to complete. If it times out or throws, the test does not start. Override:
export const options = {
setupTimeout: '120s',
teardownTimeout: '60s',
vus: 10,
duration: '30s',
};Important: if setup() throws an error, K6 will still call teardown() before exiting. This means teardown can always attempt cleanup even if setup only partially succeeded — useful when setup creates database records before the part that fails.
If setup() calls check() and the check fails, K6 continues (checks never abort the test). Add a fail() call if you want setup failure to abort the test run:
import { check, fail } from 'k6';
export function setup() {
const res = http.post('/auth/login', { ... });
if (!check(res, { 'login ok': (r) => r.status === 200 })) {
fail('Login failed in setup — aborting test');
}
return { token: res.json('token') };
}What should NOT go in setup()
setup() runs in a single goroutine with no VU context. It is not the place for:
- Warmup requests. Requests in setup() count in your metrics but are not driven by the VU model. They add noise to your results.
- Large parallel operations. There is no concurrency in setup — all HTTP calls run sequentially.
- Load test logic. Keep setup focused: get credentials, confirm the environment is reachable, create the minimal test data needed.
⚠️ Common mistakes
- Not returning anything from setup() and wondering why
datais undefined. If setup() has no return statement,datain the default function isundefined. Always return an object, even{}if there is nothing to pass. - Mutating the data object inside the default function. K6 passes a copy to each VU — mutations do not propagate between VUs and do not persist across iterations. Treat
dataas read-only. - Putting expensive requests in setup to warm the cache. If setup makes 50 API calls to create test users, it warms caches and pre-heats connection pools in ways that make your subsequent load test results look better than production reality. Minimise setup's footprint.
🎯 Practice task
Build a setup/teardown workflow against a public test API. 30 minutes.
Use https://jsonplaceholder.typicode.com — a free, public REST API that does not require authentication.
- Write
export function setup()that callsGET /users/1and returns{ userId: 1, name: res.json('name') }. - Write
export default function (data)that callsGET /posts?userId=${data.userId}and adds two checks: status is 200 and the response body contains at least one post. - Write
export function teardown(data)that logs'Test completed for user: ' + data.name. - Set
vus: 3, duration: '20s'in options. Run and confirm: the setup log appears once, the default function runs many times, the teardown log appears once at the end. - Deliberately make the setup call fail by targeting
/users/99999(non-existent). Observe whether teardown still runs.