k6 actually runs all your tests in parallel by default, even if you’re only defining a single scenario.
Let’s say you have a complex load test scenario with multiple distinct user flows – maybe signing up, logging in, browsing products, and adding to cart. You want to measure the performance of each of these flows independently, but also see the aggregate performance. This is where k6’s groups and tags come in. They’re not just for reporting; they fundamentally shape how k6 executes and measures your test.
Imagine this k6 script:
import http from 'k6/http';
import { group, check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '1m',
};
export default function () {
group('Signup Flow', function () {
const res = http.get('https://example.com/signup');
check(res, {
'signup page loaded': (r) => r.status === 200,
});
sleep(1);
});
group('Login Flow', function () {
const res = http.post('https://example.com/login', {
username: 'testuser',
password: 'password123',
});
check(res, {
'login successful': (r) => r.status === 200,
});
sleep(0.5);
});
group('Product Browsing', function () {
const res = http.get('https://example.com/products');
check(res, {
'products page loaded': (r) => r.status === 200,
});
sleep(2);
});
}
When k6 runs this, it doesn’t execute the Signup Flow group, then the Login Flow, and so on, sequentially within a single VU. Instead, each group declaration acts as a potential "scenario" for k6’s internal scheduler. If you have multiple groups like this within a single default function, k6 will distribute VUs across all of them. The vus: 10 in options means 10 VUs in total will be running, and they will be dynamically assigned to execute iterations of whichever group is currently "available" or needs more load.
tags extend this by allowing you to attach arbitrary key-value pairs to every single metric emitted by a particular group or HTTP request. This is crucial for filtering and slicing your results later.
Here’s how you’d add tags:
import http from 'k6';
import { group, check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '1m',
};
export default function () {
group('Signup Flow', function () {
const res = http.get('https://example.com/signup', {
tags: {
service: 'auth',
flow: 'signup',
},
});
check(res, {
'signup page loaded': (r) => r.status === 200,
}, {
flow: 'signup', // tags for checks
});
sleep(1);
});
group('Login Flow', function () {
const res = http.post('https://example.com/login', {
username: 'testuser',
password: 'password123',
}, {
tags: {
service: 'auth',
flow: 'login',
},
});
check(res, {
'login successful': (r) => r.status === 200,
}, {
flow: 'login',
});
sleep(0.5);
});
}
Notice how tags can be applied to http.get/post calls and check calls. When you run this, k6 will emit metrics for http_req_duration, http_req_failed, checks, etc., and each of these metrics will automatically have the service: auth, flow: signup (or login) tags attached.
The real power emerges when you run this with k6’s output options. For example, sending to InfluxDB or Prometheus:
k6 run --out influxdb=http://localhost:8086/k6 --out prometheus script.js
In your monitoring dashboard (like Grafana), you can then query for metrics like:
http_req_duration{service="auth", flow="signup"}
This allows you to isolate the performance of just the signup requests, or just the login requests, across all your VUs and iterations. You can see the average duration, error rate, and latency for each specific part of your complex scenario.
The system solves the problem of needing to understand the performance characteristics of discrete user journeys within a larger, simulated user base. Without groups and tags, you’d only see a blended average of all requests, making it impossible to pinpoint bottlenecks in specific workflows.
What most people don’t realize is that the group function itself doesn’t create separate VUs or scenarios in the way the scenarios option does. It’s a way to logically partition the work within a single k6 execution context (a VU’s iteration). When k6 sees multiple groups within the default function, it treats them as potential "tasks" that VUs can pick up. If one group has many more iterations scheduled or takes longer, VUs will naturally spend more time executing that group’s code. The scenarios option is where you explicitly define how many VUs are dedicated to which set of execution logic, and you can assign groups to specific scenarios.
The next concept you’ll want to explore is how to use the scenarios option to explicitly define and control different VUs and durations for each group, rather than relying on k6’s default distribution.