Build data-driven k6 tests using SharedArray and CSV files, implement realistic user think time, parameterize requests with random and sequential data, and avoid the thundering herd anti-pattern.
Load tests that hammer a single URL with the same request produce unrealistic results: the CDN caches everything, the database query plan caches, and you measure the performance of your caching layer, not your application. Real users send different requests, wait between actions, and follow unpredictable paths through your application.
Data-driven tests parameterize requests with realistic data — different users, products, and search terms. Combined with realistic think time and user flow simulation, they produce load test results that accurately predict production performance.
k6 runs each VU in a separate goroutine. Without SharedArray, each VU parses its own copy of a JSON file — 500 VUs loading a 10 MB user list means 5 GB of RAM.
SharedArray parses the data once in a shared memory space and gives all VUs read-only access. It is the only way to load large datasets efficiently.
1import { SharedArray } from 'k6/data';
2
3// Data is parsed ONCE regardless of VU count
4const users = new SharedArray('users', function () {
5 // This function runs once in the init stage
6 return JSON.parse(open('./data/users.json'));
7 // OR for CSV: parse manually or use papaparse
8});
9
10// Load CSV file
11const products = new SharedArray('products', function () {
12 const csv = open('./data/products.csv');
13 return csv
14 .split('\n')
15 .slice(1) // skip header row
16 .filter(Boolean)
17 .map((line) => {
18 const [id, name, price, category] = line.split(',');
19 return { id, name, price: parseFloat(price), category };
20 });
21});
22
23export default function () {
24 // Pick a random user and product
25 const user = users[Math.floor(Math.random() * users.length)];
26 const product = products[Math.floor(Math.random() * products.length)];
27
28 // Each VU sends a unique combination
29 const res = http.post('/api/cart', JSON.stringify({
30 userId: user.id,
31 productId: product.id,
32 quantity: Math.ceil(Math.random() * 3),
33 }), { headers: { 'Content-Type': 'application/json' } });
34
35 check(res, { 'cart updated': (r) => r.status === 201 });
36}Constant 1-second sleep between every request does not model real users. Real users follow an exponential distribution: most actions are fast, some take minutes.
1import { sleep } from 'k6';
2
3// WRONG: unrealistic constant sleep — creates a "thundering herd"
4// All 100 VUs send a request at t=0, t=1, t=2... simultaneously
5function badThinkTime() {
6 sleep(1);
7}
8
9// CORRECT: uniform random sleep (simple, reasonable)
10function uniformSleep(min = 0.5, max = 3) {
11 sleep(min + Math.random() * (max - min));
12}
13
14// BETTER: exponential random sleep (models human behavior more accurately)
15// Mean = 1 / lambda; most values are small, some are large
16function exponentialSleep(lambda = 1) {
17 sleep(-Math.log(Math.random()) / lambda);
18}
19
20// ADVANCED: Gaussian (normal) distribution around a mean
21function gaussianSleep(mean = 2, stddev = 0.5) {
22 // Box-Muller transform
23 const u1 = Math.random();
24 const u2 = Math.random();
25 const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
26 sleep(Math.max(0, mean + z * stddev));
27}
28
29// Usage in test:
30export default function () {
31 http.get('https://api.example.com/products');
32 uniformSleep(0.5, 3); // think time before next action
33
34 http.get('https://api.example.com/products/123');
35 exponentialSleep(0.5); // mean ~2s, occasionally 5-10s
36}For tests where each VU must use a unique credential (to avoid session conflicts), use __VU to assign data slices deterministically.
1import { SharedArray } from 'k6/data';
2
3const users = new SharedArray('users', () => JSON.parse(open('./data/perf-users.json')));
4
5export default function () {
6 // __VU is 1-indexed; __ITER is 0-indexed iteration count for this VU
7 // Each VU uses its own user — no session conflicts
8 const vuIndex = (__VU - 1) % users.length;
9 const user = users[vuIndex];
10
11 console.log(`VU ${__VU} iteration ${__ITER} using user ${user.email}`);
12
13 // First iteration: login and cache token in VU-local variable
14 // (module-level variables are shared, VU-local variables use closure)
15 const loginRes = http.post('/api/auth/login',
16 JSON.stringify({ email: user.email, password: user.password }),
17 { headers: { 'Content-Type': 'application/json' } }
18 );
19
20 check(loginRes, { 'login ok': (r) => r.status === 200 });
21}
22
23// data/perf-users.json — pre-created test accounts
24// [
25// { "id": "u001", "email": "[email protected]", "password": "PerfPass123!" },
26// { "id": "u002", "email": "[email protected]", "password": "PerfPass123!" },
27// ...100 entries...
28// ]Caching auth tokens across iterations: Store the token in a module-level object keyed by VU ID to avoid logging in on every iteration:
1const tokenCache = {};
2
3export default function () {
4 if (!tokenCache[__VU]) {
5 const res = http.post('/api/auth/login', credentials);
6 tokenCache[__VU] = res.json('accessToken');
7 }
8 const token = tokenCache[__VU];
9 http.get('/api/dashboard', { headers: { Authorization: `Bearer ${token}` } });
10}Real users search for different terms, apply different filters, and browse different pages. Parameterize all of these.
1import { SharedArray } from 'k6/data';
2
3const searchTerms = new SharedArray('search', () => [
4 'javascript', 'typescript', 'react', 'node.js', 'docker',
5 'kubernetes', 'postgresql', 'redis', 'graphql', 'next.js',
6]);
7
8const categories = ['frontend', 'backend', 'devops', 'database', 'security'];
9const sortOptions = ['newest', 'rating', 'popular', 'price-asc'];
10const pageSizes = [10, 20, 50];
11
12export default function () {
13 // Random combination for each request
14 const term = searchTerms[Math.floor(Math.random() * searchTerms.length)];
15 const category = categories[Math.floor(Math.random() * categories.length)];
16 const sort = sortOptions[Math.floor(Math.random() * sortOptions.length)];
17 const pageSize = pageSizes[Math.floor(Math.random() * pageSizes.length)];
18 const page = Math.ceil(Math.random() * 5); // pages 1-5
19
20 const params = new URLSearchParams({ q: term, category, sort, limit: String(pageSize), page: String(page) });
21 const res = http.get(
22 `https://api.example.com/search?${params}`,
23 { tags: { name: 'search' } }
24 );
25
26 check(res, {
27 'search 200': (r) => r.status === 200,
28 'results array': (r) => Array.isArray(r.json('results')),
29 });
30
31 sleep(0.5 + Math.random() * 2);
32}Real users follow journey paths, not random endpoint hammering. Model journeys with probability-based branching.
1export default function () {
2 // 100% of users browse the home page
3 http.get('https://api.example.com/courses', { tags: { name: 'home' } });
4 sleep(1 + Math.random());
5
6 // 60% search for a specific course
7 if (Math.random() < 0.60) {
8 const term = searchTerms[Math.floor(Math.random() * searchTerms.length)];
9 http.get(`https://api.example.com/search?q=${encodeURIComponent(term)}`,
10 { tags: { name: 'search' } });
11 sleep(0.5 + Math.random());
12 }
13
14 // 40% view a course detail page
15 if (Math.random() < 0.40) {
16 const courseId = `course_${Math.ceil(Math.random() * 100)}`;
17 http.get(`https://api.example.com/courses/${courseId}`,
18 { tags: { name: 'course-detail' } });
19 sleep(2 + Math.random() * 3); // users read for a while
20 }
21
22 // 15% enroll in a course
23 if (Math.random() < 0.15) {
24 http.post('https://api.example.com/progress/enroll',
25 JSON.stringify({ courseId: 'course_42' }),
26 { headers: { 'Content-Type': 'application/json' },
27 tags: { name: 'enroll' } });
28 }
29}
30// This produces realistic traffic distribution:
31// home: 100% → search: 60% → detail: 40% → enroll: 15%
32// matching real analytics funnel dataComplete data-driven k6 test loading user credentials from CSV, simulating a realistic funnel, and implementing cache-aware token management.
1// tests/perf/data-driven-load.js
2// Uses: tests/perf/data/perf-users.csv (100 test accounts)
3// CSV format: id,email,password,tier
4// Run: k6 run tests/perf/data-driven-load.js
5
6import http from 'k6/http';
7import { check, sleep, group } from 'k6';
8import { SharedArray } from 'k6/data';
9import { Rate, Counter } from 'k6/metrics';
10
11const users = new SharedArray('perf-users', function () {
12 return open('./data/perf-users.csv')
13 .split('\n')
14 .slice(1) // skip CSV header
15 .filter(Boolean)
16 .map((line) => {
17 const [id, email, password, tier] = line.split(',');
18 return { id, email, password, tier };
19 });
20});
21
22const courseIds = new SharedArray('courses', function () {
23 return JSON.parse(open('./data/course-ids.json'));
24});
25
26const searchTerms = new SharedArray('search', function () {
27 return ['react', 'typescript', 'node.js', 'postgresql', 'docker',
28 'kubernetes', 'graphql', 'redis', 'testing', 'security'];
29});
30
31const enrollmentErrors = new Rate('enrollment_errors');
32const searchRequests = new Counter('search_requests');
33
34export const options = {
35 scenarios: {
36 mixed_journey: {
37 executor: 'ramping-vus',
38 startVUs: 1,
39 stages: [
40 { duration: '2m', target: 30 },
41 { duration: '10m', target: 50 },
42 { duration: '2m', target: 0 },
43 ],
44 },
45 },
46 thresholds: {
47 http_req_duration: ['p(95)<800'],
48 http_req_failed: ['rate<0.005'],
49 enrollment_errors: ['rate<0.01'],
50 },
51};
52
53// VU-local token cache — avoids login on every iteration
54const tokenCache = {};
55
56const BASE = __ENV.BASE_URL || 'http://localhost:3000';
57
58function getToken(user) {
59 if (tokenCache[__VU]) return tokenCache[__VU];
60
61 const res = http.post(
62 `${BASE}/api/auth/login`,
63 JSON.stringify({ email: user.email, password: user.password }),
64 { headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } }
65 );
66
67 if (res.status === 200) {
68 tokenCache[__VU] = res.json('accessToken');
69 }
70 return tokenCache[__VU] || null;
71}
72
73export default function () {
74 // Each VU gets a consistent user based on its VU number
75 const user = users[(__VU - 1) % users.length];
76 const token = getToken(user);
77 if (!token) { sleep(1); return; }
78
79 const h = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
80
81 // Step 1: Browse course catalog (100% of users)
82 group('Browse catalog', () => {
83 const res = http.get(`${BASE}/api/courses?limit=12`, { headers: h, tags: { name: 'course-list' } });
84 check(res, { 'courses 200': (r) => r.status === 200 });
85 });
86 sleep(1 + Math.random());
87
88 // Step 2: Search (70% probability)
89 if (Math.random() < 0.70) {
90 const term = searchTerms[Math.floor(Math.random() * searchTerms.length)];
91 group('Search', () => {
92 const res = http.get(`${BASE}/api/search?q=${encodeURIComponent(term)}&limit=10`,
93 { headers: h, tags: { name: 'search' } });
94 check(res, { 'search 200': (r) => r.status === 200 });
95 searchRequests.add(1);
96 });
97 sleep(0.5 + Math.random() * 1.5);
98 }
99
100 // Step 3: View course detail (50% probability)
101 if (Math.random() < 0.50) {
102 const courseId = courseIds[Math.floor(Math.random() * courseIds.length)];
103 group('Course detail', () => {
104 const res = http.get(`${BASE}/api/courses/${courseId}`,
105 { headers: h, tags: { name: 'course-detail' } });
106 check(res, { 'detail 200': (r) => r.status === 200 });
107 });
108 sleep(2 + Math.random() * 4); // read time
109 }
110
111 // Step 4: Enroll (20% probability)
112 if (Math.random() < 0.20) {
113 const courseId = courseIds[Math.floor(Math.random() * courseIds.length)];
114 group('Enroll', () => {
115 const res = http.post(`${BASE}/api/progress/enroll`,
116 JSON.stringify({ courseId }),
117 { headers: h, tags: { name: 'enroll' } });
118 enrollmentErrors.add(res.status >= 400);
119 check(res, { 'enroll ok': (r) => r.status === 201 || r.status === 200 });
120 });
121 }
122
123 sleep(1 + Math.random() * 2);
124}This data-driven test loads 100 test users from a CSV file and course IDs from JSON using SharedArray for memory efficiency. The VU-local tokenCache avoids re-authentication on every iteration. Each VU uses a deterministic user slice via (__VU - 1) % users.length to prevent session conflicts. The user journey follows realistic funnel probabilities: 100% browse → 70% search → 50% view detail → 20% enroll. This distribution should match your actual analytics data. The variable sleep times prevent thundering-herd synchronization.
The next lesson covers k6 extensions and the Grafana Cloud k6 service — extending k6 with browser testing, gRPC support, Kafka metrics output, and running distributed cloud tests.
Your progress is saved in this browser, on this device only.