Loading External Data — JSON, CSV

8 min read

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 directory

Run 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 throws GoError: open() calls are not allowed in the default function. Move all open() 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. Use SharedArray for any file you would not be comfortable loading N times simultaneously.
  • Importing Node.js fs or path modules. They do not exist in K6's runtime. The error is GoError: Cannot find module 'fs' — replace with open().
  • Relative paths from the wrong directory. If your script is in tests/ and your data is in data/, 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.

  1. Create a file users.json with at least 5 user objects: [{ "username": "user1", "role": "viewer" }, ...].
  2. Write a K6 script that loads users.json with JSON.parse(open('./users.json')) in the init context.
  3. In the default function, pick a random user and make a GET request to https://httpbin.org/get with a query parameter ?user=${user.username}. Check that the response contains the username in the args object.
  4. Add a console.log showing which user was selected on each iteration. Run with vus: 3, iterations: 9. Confirm 9 log lines with a mix of users.
  5. Create users.csv with the same data: a header row username,role and 5 data rows. Load it with either manual parsing or papaparse. Confirm the same test works with CSV input.
  6. Change from random selection to users[(__VU - 1) % users.length]. Run again. Confirm each VU always selects the same user.

// tip to track lessons you complete and pick up where you left off across devices.