Headers, Cookies, and Authentication

9 min read

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 },
  });
}

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 the vuToken pattern for per-VU sessions that log in once.
  • Sharing a CookieJar instance across VUs. Creating a CookieJar in 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 with open() 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.

  1. Write a script with export function setup() that makes a POST to https://httpbin.org/post with { username: 'testuser', password: 'testpass' } as the body. Return { token: 'simulated-token-12345' } (simulate a real login response).
  2. In export default function (data), make a GET to https://httpbin.org/headers with an Authorization: Bearer ${data.token} header. Parse the response with res.json() and add a check that res.json('headers.Authorization') equals 'Bearer simulated-token-12345'.
  3. Add a Basic auth request: compute encoding.b64encode('user:pass') in init code (outside any function). Make a GET to https://httpbin.org/headers with the Basic auth header. Verify the Authorization header appears correctly in the response.
  4. Add a cookie test: make a POST to https://httpbin.org/cookies/set?session=abc123. Then make a GET to https://httpbin.org/cookies. Check that the response JSON shows the session cookie.
  5. Run with vus: 3, duration: '20s'. Count the number of times setup() logs vs the number of VU iterations.

// tip to track lessons you complete and pick up where you left off across devices.