Authentication is present in almost every real-world load test. Getting it wrong — authenticating per VU per iteration instead of once — distorts your results and floods your auth endpoint with artificial traffic. This lesson covers every common pattern: Bearer tokens, Basic auth, cookies, and multi-user credential pools.
Shared vs per-VU authentication
Auth patterns in K6
Shared auth (via setup)
One login request for the entire test
Token passed through data parameter
All VUs use the same credentials
Best for read-heavy tests or single-user scenarios
Auth endpoint load: 1 request total
Per-VU auth (module-level variable)
Each VU logs in once on first iteration
Token stored in module-level variable per VU
Each VU uses its own credentials
Best for tests that need distinct user sessions
Auth endpoint load: 1 request per VU
Bearer token authentication
The most common pattern: use setup() to obtain a token and pass it to all VUs through the data parameter.
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') };
}
export default function (data) {
const res = http.get('https://api.example.com/orders', {
headers: { Authorization: `Bearer ${data.token}` },
});
check(res, { 'orders fetched': (r) => r.status === 200 });
}One login request. Five hundred VUs. All of them get the same token through data.token.
Basic authentication
K6's k6/encoding module encodes credentials for Basic auth headers:
import http from 'k6/http';
import encoding from 'k6/encoding';
export default function () {
const credentials = encoding.b64encode('username:password');
const res = http.get('https://api.example.com/protected', {
headers: { Authorization: `Basic ${credentials}` },
});
}If the credentials are constant, compute encoding.b64encode() once in the init context (outside any function) and reuse the result rather than re-encoding on every iteration.
import encoding from 'k6/encoding';
// Computed once per VU in init context — not per iteration
const AUTH_HEADER = `Basic ${encoding.b64encode('loadtest:TestPass@1234')}`;
export default function () {
http.get('https://api.example.com/resource', {
headers: { Authorization: AUTH_HEADER },
});
}Cookie management
K6 manages cookies automatically per VU — each VU has its own isolated cookie jar, just like a separate browser session. When a response sets a Set-Cookie header, K6 stores it and sends it on subsequent requests to the same domain.
export default function () {
// Login — server sets a session cookie
http.post('https://app.example.com/login', JSON.stringify({
username: 'testuser',
password: 'TestPass@1234',
}), { headers: { 'Content-Type': 'application/json' } });
// Subsequent requests automatically include the session cookie
const dashboardRes = http.get('https://app.example.com/dashboard');
check(dashboardRes, { 'dashboard loaded': (r) => r.status === 200 });
}To set a cookie explicitly:
import { CookieJar } from 'k6/http';
const jar = new CookieJar();
jar.set('https://app.example.com', 'session_id', 'abc123', {
domain: 'app.example.com',
path: '/',
secure: true,
httpOnly: false,
});
export default function () {
http.get('https://app.example.com/dashboard', { jar });
}Per-VU authentication with credential pools
When your test requires different users — for scenarios where shared tokens would hit user-specific rate limits, or where tests must behave differently per user — use a SharedArray of credentials and assign one per VU:
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', function () {
return [
{ email: 'user1@test.com', password: 'Pass1234!' },
{ email: 'user2@test.com', password: 'Pass1234!' },
{ email: 'user3@test.com', password: 'Pass1234!' },
// ... more users
];
});
// Module-level variable — each VU has its own copy
let vuToken = null;
export default function () {
// Each VU logs in exactly once
if (vuToken === null) {
const user = users[(__VU - 1) % users.length];
const res = http.post('https://api.example.com/auth/login',
JSON.stringify(user),
{ headers: { 'Content-Type': 'application/json' } }
);
check(res, { 'VU login succeeded': (r) => r.status === 200 });
vuToken = res.json('token');
}
const res = http.get('https://api.example.com/profile', {
headers: { Authorization: `Bearer ${vuToken}` },
});
check(res, { 'profile loaded': (r) => r.status === 200 });
}vuToken starts as null in each VU's isolated context. The first iteration logs in and sets it. Every subsequent iteration skips the login. This gives you N unique users (one per VU) with only N login requests — not N × iterations.
Passing custom headers
Any object passed as headers inside the params argument is merged into the request headers:
export default function (data) {
const params = {
headers: {
Authorization: `Bearer ${data.token}`,
'Content-Type': 'application/json',
'X-Request-ID': `k6-${__VU}-${__ITER}`,
'Accept-Language': 'en-US',
},
};
http.get('https://api.example.com/orders', params);
}Headers are case-insensitive in HTTP but K6 passes them as you write them. Use the standard capitalisation (Content-Type, Authorization) to avoid surprises with servers that parse headers strictly.
⚠️ Common mistakes
- Logging in inside the default function with 100+ VUs. With 100 VUs and a 5-second iteration loop, that is 20 login requests per second — aimed entirely at the auth endpoint. Use
setup()for a single shared token, or thevuTokenpattern for per-VU sessions that log in once. - Sharing a
CookieJarinstance across VUs. Creating aCookieJarin init code creates one instance per VU (since init runs per VU). Creating one inside the default function creates a new jar per iteration, discarding cookies between requests. Create it outside any function if you need session persistence across requests within an iteration. - Hard-coding credentials in the script. Use environment variables (
__ENV.PASSWORD) or a CSV file loaded withopen()so credentials are not committed to version control.
🎯 Practice task
Implement three different authentication patterns against a test API. 35 minutes.
Use https://jsonplaceholder.typicode.com for unauthenticated requests, and https://httpbin.org for header inspection.
- Write a script with
export function setup()that makes a POST tohttps://httpbin.org/postwith{ username: 'testuser', password: 'testpass' }as the body. Return{ token: 'simulated-token-12345' }(simulate a real login response). - In
export default function (data), make a GET tohttps://httpbin.org/headerswith anAuthorization: Bearer ${data.token}header. Parse the response withres.json()and add a check thatres.json('headers.Authorization')equals'Bearer simulated-token-12345'. - Add a Basic auth request: compute
encoding.b64encode('user:pass')in init code (outside any function). Make a GET tohttps://httpbin.org/headerswith the Basic auth header. Verify the Authorization header appears correctly in the response. - Add a cookie test: make a POST to
https://httpbin.org/cookies/set?session=abc123. Then make a GET tohttps://httpbin.org/cookies. Check that the response JSON shows thesessioncookie. - Run with
vus: 3, duration: '20s'. Count the number of timessetup()logs vs the number of VU iterations.