Skip to main content
Every k6 test follows a structured lifecycle with four distinct phases. Understanding these phases is essential for writing effective load tests.

Test Lifecycle Phases

k6 executes tests in the following order:
1

Init Phase

Code in the init context runs once per VU at the beginning of the test.
2

Setup Phase

The setup() function runs once before the test starts.
3

VU Phase (Default Function)

The default function executes repeatedly for each VU iteration.
4

Teardown Phase

The teardown() function runs once after the test completes.

Init Context

The init context is where k6 prepares your test script. Code at the top level of your script runs during initialization.
import http from 'k6/http';
import { check } from 'k6';
import { Counter } from 'k6/metrics';

// Init context - runs once per VU
const myCounter = new Counter('my_counter');

export const options = {
  vus: 10,
  duration: '30s',
};

console.log('Init stage'); // Runs once per VU

What Happens in Init

Based on the k6 source code in internal/js/bundle.go, the init context:
  • Imports modules
  • Defines the default function and options
  • Creates custom metrics
  • Loads local files
  • Initializes module-provided types
The init context runs once per VU. With 10 VUs, init code executes 10 times total.
Do not make HTTP requests in the init context. They won’t be measured and will slow down test startup.

Setup Phase

The optional setup() function runs once before the test begins, regardless of VU count.
export function setup() {
  // Runs once before the test starts
  const res = http.post('https://api.example.com/login', {
    username: 'admin',
    password: 'secret',
  });
  
  // Return data to be used by VUs
  return { token: res.json('token') };
}

export default function(data) {
  // Use setup data
  const headers = { 'Authorization': `Bearer ${data.token}` };
  http.get('https://api.example.com/users', { headers });
}

export function teardown(data) {
  // Clean up using setup data
  http.post('https://api.example.com/logout', null, {
    headers: { 'Authorization': `Bearer ${data.token}` }
  });
}

Setup Use Cases

  • Authenticate and obtain tokens
  • Prepare test data
  • Configure the system under test
  • Retrieve configuration values
Setup data is serialized to JSON and distributed to all VUs. Keep it lightweight.

Default Function (VU Phase)

The default function is the heart of your test. Each VU executes it repeatedly for the duration of the test.
export default function() {
  // This runs many times per VU
  const response = http.get('https://quickpizza.grafana.com/api/pizza');
  
  check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 200ms': (r) => r.timings.duration < 200,
  });
  
  sleep(1);
}

Iteration Behavior

From the execution state implementation in lib/execution.go:
  • Each VU is tracked with counters for full and interrupted iterations
  • fullIterationsCount increments when iterations complete normally
  • interruptedIterationsCount increments when iterations are cut short
  • The activeVUs counter tracks currently executing VUs
An iteration is the complete execution of the default function, from start to finish.

Teardown Phase

The optional teardown() function runs once after all VUs finish executing.
export function teardown(data) {
  // Runs once at the end of the test
  console.log('Test completed');
  
  // Clean up resources
  http.post('https://api.example.com/cleanup', {
    testId: data.testId
  });
}

Teardown Use Cases

  • Clean up test data
  • Log out or revoke tokens
  • Reset system state
  • Perform final validation
If the test is aborted (Ctrl+C), teardown may not execute. Design your tests to handle incomplete cleanup.

Execution Flow Diagram

Init Context (once per VU)

Setup (once)

┌─────────────────────┐
│  VU 1: default()    │
│  VU 2: default()    │  ← Runs repeatedly
│  VU 3: default()    │    until duration/iterations complete
│  ...                │
└─────────────────────┘

Teardown (once)

Execution Status

k6 tracks test execution through multiple states defined in lib/execution.go:
  • ExecutionStatusCreated - Test created
  • ExecutionStatusInitVUs - Initializing VUs
  • ExecutionStatusInitExecutors - Initializing executors
  • ExecutionStatusInitDone - Initialization complete
  • ExecutionStatusSetup - Running setup
  • ExecutionStatusRunning - Default function executing
  • ExecutionStatusTeardown - Running teardown
  • ExecutionStatusEnded - Test complete

Best Practices

1

Keep Init Lightweight

Avoid expensive operations in init. Use it for imports and metric definitions only.
2

Use Setup for Authentication

Obtain tokens once in setup and distribute to VUs, rather than authenticating in every iteration.
3

Make Default Function Repeatable

Ensure the default function can run many times without side effects.
4

Handle Teardown Failures

Don’t rely on teardown for critical cleanup - it may not run if the test is interrupted.

Complete Example

import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate } from 'k6/metrics';

// Init context
const errorRate = new Rate('errors');

export const options = {
  stages: [
    { duration: '30s', target: 10 },
    { duration: '1m', target: 10 },
    { duration: '30s', target: 0 },
  ],
};

export function setup() {
  // Authenticate once
  const loginRes = http.post('https://api.example.com/login', {
    username: 'testuser',
    password: 'testpass',
  });
  
  return { authToken: loginRes.json('token') };
}

export default function(data) {
  const params = {
    headers: { 'Authorization': `Bearer ${data.authToken}` },
  };
  
  // Make requests
  const res = http.get('https://api.example.com/data', params);
  
  // Validate response
  const success = check(res, {
    'status is 200': (r) => r.status === 200,
  });
  
  errorRate.add(!success);
  sleep(1);
}

export function teardown(data) {
  // Logout
  http.post('https://api.example.com/logout', null, {
    headers: { 'Authorization': `Bearer ${data.authToken}` },
  });
}
Understanding the test lifecycle helps you structure tests efficiently and avoid common pitfalls like making unnecessary requests or reinitializing data in every iteration.

Build docs developers (and LLMs) love