Load tests that always use the same credentials, the same product IDs, or the same search terms do not reflect real traffic. Real users are different. This lesson covers how to bring external data into K6 using its built-in open() function — and why you cannot use Node.js's fs module.
K6 is not Node.js
K6 runs on Goja, a Go-based JavaScript runtime. The Node.js standard library — including fs, path, and require() — is not available. Attempting to import them throws a module-not-found error. K6 provides its own file-loading mechanism: open().
// ❌ Does not work — K6 is not Node.js
const fs = require('fs');
const data = fs.readFileSync('./users.json', 'utf-8');
// ✅ Correct — K6's built-in file loader
const data = open('./users.json');open() is only allowed in the init context (outside any function). K6 enforces this strictly and throws an error if you call open() inside default() or setup().
The data loading flow
Memory warning: open() and its subsequent parse run once per VU. With 100 VUs loading a 1MB JSON file, K6 allocates 100MB of memory — one copy per VU. For files larger than a few hundred KB, use SharedArray instead (next lesson).
Loading JSON
import http from 'k6/http';
import { check } from 'k6';
// open() returns raw string — JSON.parse converts to array/object
const users = JSON.parse(open('./users.json'));
export const options = { vus: 10, duration: '30s' };
export default function () {
// Random element — different user each iteration
const user = users[Math.floor(Math.random() * users.length)];
const res = http.post('https://api.example.com/login',
JSON.stringify({ email: user.email, password: user.password }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(res, { 'login succeeded': (r) => r.status === 200 });
}The users.json file lives in the same directory as the script:
[
{ "email": "user1@test.com", "password": "TestPass1!" },
{ "email": "user2@test.com", "password": "TestPass2!" },
{ "email": "user3@test.com", "password": "TestPass3!" }
]Loading CSV — manual parsing
K6 has no built-in CSV parser. For simple CSV files you can parse manually:
function parseCSV(text) {
const lines = text.trim().split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const values = line.split(',');
return Object.fromEntries(headers.map((h, i) => [h.trim(), values[i].trim()]));
});
}
const users = parseCSV(open('./users.csv'));
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
// use user.email, user.password, etc.
}This works for simple CSV files without quoted fields, embedded commas, or multi-line values.
Loading CSV — with papaparse
For real-world CSV files, use the papaparse library from jslib.k6.io — K6's community library hosting service:
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
const csvData = open('./users.csv');
const users = papaparse.parse(csvData, { header: true, skipEmptyLines: true }).data;
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
http.post('https://api.example.com/login',
JSON.stringify({ email: user.email, password: user.password }),
{ headers: { 'Content-Type': 'application/json' } }
);
}header: true uses the first CSV row as property names, producing the same object shape as the JSON example above. skipEmptyLines: true prevents empty trailing lines from becoming undefined entries.
Deterministic distribution — same user per VU
For scenarios where each VU should consistently represent the same user (simulating persistent sessions), use __VU as the index:
const users = JSON.parse(open('./users.json'));
export default function () {
// VU 1 always uses users[0], VU 2 always uses users[1], etc.
const user = users[(__VU - 1) % users.length];
// ...
}(__VU - 1) converts the 1-based VU ID to a 0-based array index. The modulo wraps when VU count exceeds the array length.
Specifying file paths
open() resolves paths relative to the script file, not the current working directory:
const data = open('./data/users.json'); // ./data/ relative to script
const data2 = open('../shared/products.csv'); // parent directoryRun K6 from the directory containing the script, or use an absolute path. Relative paths resolve from the script file location, which can cause confusion if you run k6 run ./tests/script.js from the project root — ./data/users.json would be relative to ./tests/, not the project root.
⚠️ Common mistakes
- Calling
open()inside the default function. K6 throwsGoError: open() calls are not allowed in the default function. Move allopen()calls to init code — outside any function. - Not accounting for per-VU memory cost.
open()reads the file once per VU. 50 VUs × 5MB CSV = 250MB of memory. UseSharedArrayfor any file you would not be comfortable loading N times simultaneously. - Importing Node.js
fsorpathmodules. They do not exist in K6's runtime. The error isGoError: Cannot find module 'fs'— replace withopen(). - Relative paths from the wrong directory. If your script is in
tests/and your data is indata/, the path is'../data/users.csv'— not'./data/users.csv'. K6 resolves relative to the script file, not the working directory.
🎯 Practice task
Load external test data and verify it drives your requests. 30 minutes.
- Create a file
users.jsonwith at least 5 user objects:[{ "username": "user1", "role": "viewer" }, ...]. - Write a K6 script that loads
users.jsonwithJSON.parse(open('./users.json'))in the init context. - In the default function, pick a random user and make a GET request to
https://httpbin.org/getwith a query parameter?user=${user.username}. Check that the response contains the username in theargsobject. - Add a
console.logshowing which user was selected on each iteration. Run withvus: 3, iterations: 9. Confirm 9 log lines with a mix of users. - Create
users.csvwith the same data: a header rowusername,roleand 5 data rows. Load it with either manual parsing or papaparse. Confirm the same test works with CSV input. - Change from random selection to
users[(__VU - 1) % users.length]. Run again. Confirm each VU always selects the same user.