Skip to main content
The externally-controlled executor allows you to control k6 execution in real-time via the REST API. You can dynamically scale VUs up or down, pause and resume execution, and adjust test parameters while the test is running.

How It Works

With externally controlled execution:
  1. Test starts with initial vus and runs for duration (or indefinitely if duration is 0)
  2. VUs continuously loop through iterations
  3. You can control execution via the k6 REST API:
    • Scale VUs up or down
    • Pause and resume the test
    • Update the configuration dynamically
  4. Test runs until duration expires or you stop it manually
VUs ^
 50 |            API: scale to 50
 30 |     ╱‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾╲
 10 |‾‾‾‾╯                 ╲___
  0 +-----------------------------------> time
      ^         ^          ^    ^
      start   scale up   scale down
              (via API)  (via API)

Configuration

executor
string
required
Must be externally-controlled
vus
integer
default:"0"
Initial number of VUs. Can be 0. Cannot be negative.
maxVUs
integer
default:"vus"
Maximum number of VUs that can be used during the test. Once set at test start, this cannot be decreased (only increased via API).
duration
duration
required
Maximum test duration. Use 0 for infinite duration (test runs until manually stopped). Cannot be negative.
The gracefulStop option is NOT supported by this executor. VUs stop immediately when the test ends.

Example

Basic Setup

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  scenarios: {
    external: {
      executor: 'externally-controlled',
      vus: 10,
      maxVUs: 100,
      duration: '30m',  // Or '0' for infinite
    },
  },
};

export default function () {
  http.get('https://test.k6.io');
  sleep(1);
}
Run with the API enabled:
k6 run --http-debug script.js

Infinite Duration Test

export const options = {
  scenarios: {
    external: {
      executor: 'externally-controlled',
      vus: 1,
      maxVUs: 50,
      duration: '0',  // Runs until manually stopped
    },
  },
};

Start Paused

k6 run --paused script.js
Then control via API to resume when ready.

API Control

The k6 REST API (enabled with --http-debug or environment variable) provides endpoints to control execution.

Get Current Status

curl http://localhost:6565/v1/status
Response:
{
  "data": {
    "attributes": {
      "paused": false,
      "vus": 10,
      "vus-max": 100,
      "stopped": false,
      "running": true,
      "tainted": false
    }
  }
}

Scale VUs

curl -X PATCH http://localhost:6565/v1/status \
  -H 'Content-Type: application/json' \
  -d '{
    "data": {
      "attributes": {
        "vus": 50
      }
    }
  }'

Pause Test

curl -X PATCH http://localhost:6565/v1/status \
  -H 'Content-Type: application/json' \
  -d '{
    "data": {
      "attributes": {
        "paused": true
      }
    }
  }'

Resume Test

curl -X PATCH http://localhost:6565/v1/status \
  -H 'Content-Type: application/json' \
  -d '{
    "data": {
      "attributes": {
        "paused": false
      }
    }
  }'

Scale and Adjust MaxVUs

curl -X PATCH http://localhost:6565/v1/status \
  -H 'Content-Type: application/json' \
  -d '{
    "data": {
      "attributes": {
        "vus": 80,
        "vus-max": 200
      }
    }
  }'

When to Use

Use the externally controlled executor when:
  • You need dynamic control during test execution
  • You’re integrating k6 with external monitoring/alerting systems
  • You want to adjust load based on real-time application metrics
  • You’re building custom load testing dashboards
  • You need to manually respond to test conditions
  • You want to run exploratory performance testing
  • You’re implementing adaptive load testing algorithms

Behavior Details

Initial VUs and MaxVUs

At test start, k6 initializes maxVUs and activates vus of them:
{
  vus: 10,      // 10 VUs active
  maxVUs: 100,  // 100 VUs initialized and ready
}
// 10 VUs are running, 90 are initialized but idle

Dynamic Scaling

Via API, you can scale vus up to maxVUs:
// Start: vus=10, maxVUs=100
// API request: vus=50
// Result: 40 additional VUs become active (total 50 active)

MaxVUs Constraints

You can increase maxVUs via API but cannot decrease it below the initial configuration value.
// Initial config
{ vus: 10, maxVUs: 100 }

// ✓ Valid via API: increase maxVUs
{ vus: 50, maxVUs: 200 }

// ❌ Invalid via API: decrease maxVUs below initial
{ vus: 10, maxVUs: 50 }  // Error! Cannot go below 100

Duration Behavior

Finite duration:
{ duration: '30m' }
// Test runs for 30 minutes then stops
// Cannot change duration via API
Infinite duration:
{ duration: '0' }
// Test runs until you stop it (Ctrl+C or API)
// No automatic stop

Pause Behavior

When paused:
  • Running iterations complete
  • No new iterations start
  • VUs remain allocated
  • Test timer continues (for non-zero duration)
// Timeline with pause:
// 0-5m: Running normally
// 5-10m: Paused (no new iterations)
// 10-30m: Running normally
// If duration is 30m, test still ends at 30m total

Not Distributable

The externally-controlled executor does NOT support distributed execution (execution segments). It can only run on a single k6 instance.

Common Patterns

Integration with Monitoring

#!/bin/bash
# Scale based on response time

while true; do
  # Get current p95 from your monitoring system
  p95=$(curl -s http://monitoring.example.com/api/p95)
  
  if [ "$p95" -lt 1000 ]; then
    # Response time good, increase load
    curl -X PATCH http://localhost:6565/v1/status \
      -H 'Content-Type: application/json' \
      -d '{"data":{"attributes":{"vus":50}}}'
  else
    # Response time degrading, reduce load
    curl -X PATCH http://localhost:6565/v1/status \
      -H 'Content-Type: application/json' \
      -d '{"data":{"attributes":{"vus":20}}}'
  fi
  
  sleep 30
done

Manual Exploratory Testing

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  scenarios: {
    manual: {
      executor: 'externally-controlled',
      vus: 0,        // Start with no load
      maxVUs: 500,   // Allow scaling up to 500
      duration: '0', // Run indefinitely
    },
  },
};

export default function () {
  http.get('https://test.k6.io/api');
  sleep(1);
}
Then manually control:
# Start at 10 VUs to warm up
curl -X PATCH http://localhost:6565/v1/status -d '{"data":{"attributes":{"vus":10}}}'

# Observe for 2 minutes...

# Scale to 50 VUs
curl -X PATCH http://localhost:6565/v1/status -d '{"data":{"attributes":{"vus":50}}}'

# If system handles it well, scale to 100
curl -X PATCH http://localhost:6565/v1/status -d '{"data":{"attributes":{"vus":100}}}'

Dashboard Integration

// React/Node.js dashboard example
const scaleVUs = async (vuCount) => {
  await fetch('http://localhost:6565/v1/status', {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      data: {
        attributes: {
          vus: vuCount
        }
      }
    })
  });
};

const togglePause = async (paused) => {
  await fetch('http://localhost:6565/v1/status', {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      data: {
        attributes: {
          paused: paused
        }
      }
    })
  });
};

Validation

Valid Configurations

// ✓ Minimal config
{
  executor: 'externally-controlled',
  vus: 1,
  maxVUs: 10,
  duration: '10m',
}

// ✓ Infinite duration
{
  executor: 'externally-controlled',
  vus: 0,
  maxVUs: 100,
  duration: '0',
}

// ✓ maxVUs defaults to vus
{
  executor: 'externally-controlled',
  vus: 50,
  duration: '1h',
  // maxVUs automatically set to 50
}

Invalid Configurations

// ❌ Negative VUs
{
  vus: -10,  // Error!
}

// ❌ vus > maxVUs
{
  vus: 100,
  maxVUs: 50,  // Error!
}

// ❌ Negative duration
{
  duration: '-5m',  // Error!
}

// ❌ Duration not specified
{
  vus: 10,
  maxVUs: 50,
  // duration: ??? // Error!
}

// ❌ Using gracefulStop
{
  vus: 10,
  duration: '10m',
  gracefulStop: '30s',  // Error! Not supported
}

Metrics

Standard k6 metrics are emitted:
  • iterations - Total completed iterations
  • iteration_duration - Time per iteration
  • vus - Current number of active VUs (changes via API)
  • vus_max - Current maximum VUs (matches maxVUs config)

Best Practices

  1. Set high maxVUs: Allow plenty of headroom for scaling
  2. Start low: Begin with few VUs and scale up as needed
  3. Monitor carefully: Watch metrics when scaling to avoid overload
  4. Use infinite duration for exploration: duration: '0' for manual control
  5. Secure the API: Restrict access to the REST API in production
  6. Gradual scaling: Avoid jumping directly from 10 to 1000 VUs
  7. Test API integration: Ensure your control logic works before production
  8. Have a plan: Define scaling rules/criteria before running the test

Limitations

Key limitations of the externally-controlled executor:
  • No distributed execution support
  • Cannot use gracefulStop
  • Cannot decrease maxVUs below initial value
  • Cannot change duration once test starts
  • Cannot pause before test starts (use --paused flag instead)

See Also

Build docs developers (and LLMs) love