Choosing Between Virtual Users and Arrival Rate in k6
Learn when to use Virtual Users versus Arrival Rate executors in k6 to avoid coordinated omission and accurately simulate real-world traffic spikes.
28 May 2026, 00:33 UTC

The Bottleneck Blind Spot
When designing a load test, the most critical decision is whether your traffic model is closed or open. If you use a standard Virtual User (VU) approach to test a specific request-per-second (RPS) target, you risk falling into a trap called coordinated omission. This occurs when the load generator waits for a response before sending the next request; if the server slows down, the load generator slows down too, artificially hiding the system's failure to meet throughput requirements.
The takeaway: Use Virtual Users (VUs) when you want to test the maximum capacity of a system with a fixed number of concurrent clients. Use Arrival Rate executors when you must maintain a specific RPS regardless of how slow the server becomes.
Comparing Execution Models
| Feature | Closed Model (VUs) | Open Model (Arrival Rate) |
|---|---|---|
| Primary Driver | Concurrent Users | Requests per Time Unit |
| Throughput | Variable (depends on latency) | Fixed (target RPS) |
| Resource Usage | Predictable/Stable | Dynamic (VUs scale up during lag) |
| Best Use Case | Stress testing, capacity limits | SLA validation, spike testing |
The Trade-offs of Open-Model Testing
While the constant-arrival-rate executor prevents coordinated omission, it introduces a risk to the load generator itself. In a closed model, if the server response time jumps from 100ms to 10s, the throughput simply drops. In an open model, k6 will automatically spin up more VUs to maintain the target RPS despite the latency. This can lead to a sudden spike in CPU and memory consumption on the machine running k6, potentially crashing the test before the server actually fails.
Implementation: Constant Arrival Rate
To implement an open-model test, you must use the constant-arrival-rate executor. This requires defining a preAllocatedVUs pool to avoid the overhead of creating VUs during the test, and a maxVUs limit to prevent the load generator from exhausting its own resources.
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
scenarios: {
fixed_rps_test: {
executor: 'constant-arrival-rate',
rate: 100, // Target 100 requests per time unit
timeUnit: '1s', // The time unit is 1 second (100 RPS)
duration: '1m', // Total test duration
preAllocatedVUs: 20, // VUs initialized at start
maxVUs: 200, // Maximum VUs allowed to maintain rate
},
},
};
export default function () {
http.get('https://test-api.example.com/data');
}
Running and Verifying the Result
Run this script from your terminal with the following command (ensure you have k6 installed and network access to the target):
k6 run script.js
Verification Steps:
- Check the
vusmetric: If the server latency increases, you should see thevuscount rise toward themaxVUslimit. - Check
iterations: The total number of iterations should closely matchrate * duration(e.g., 100 RPS * 60s = 6,000 iterations). - Diagnostic Check: If the
vusmetric hits themaxVUsceiling and theiterationscount drops below the target, your load generator is under-provisioned or the server is too slow to sustain the target rate even with maximum concurrency.
Limitations
The Arrival Rate executor is designed for stateless request patterns. If your test requires complex per-VU state (like a unique session cookie maintained across multiple sequential requests), the dynamic scaling of VUs in an open model can make session management unpredictable. In such cases, a closed model with carefully tuned sleep intervals may be necessary, though it will not provide a true RPS guarantee.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.