Skip to main content

How to Create Data-Driven Load Testing Scripts for Complex User Journeys (2026 Guide)

How to Create Data-Driven Load Testing Scripts for Complex User Journeys (2026 Guide)

A data-driven load test feeds each virtual user different data, such as a different account, search term or product, and lets journeys branch, instead of replaying one recorded user. It matters because one fixed user hits the same cache entries, the same database rows and the same session, so the system looks faster than it is. This post covers choosing data, assigning it so users do not collide, and modelling journeys, with k6 and Locust examples.

Key takeaways

  • A script that replays one fixed user hides caching effects and looks faster than reality.
  • Drive the journey from a data set so each virtual user takes a different path with different inputs.
  • Model the branch points that matter: login state, cart contents, search terms.

Why does one fixed user make a load test misleading?

A script that logs in as the same account and fetches the same product on every iteration asks the system the same question thousands of times. The first answer lands in a cache, and the rest are cheap. The database sees one hot row. The result is a fast, flat response time curve that describes a system with one customer.

Real traffic is spread across many accounts and many items, so caches miss more often, queries touch different pages of an index, and more of the working set has to be held in memory. A fixed-user test can also fail in the other direction: if every virtual user logs in as the same account, the application may invalidate the earlier session, lock the account or serialise updates to one row, and you measure a lock you will never see in production.

The point of data-driven scripting is to remove that artefact. The data does not need to be large to help, but it does need to be varied in the ways that change the work the system does.

What data should a data-driven script use?

Pick the inputs that change the work done, then give each its own source:

InputWhy it mattersWhere to get it
AccountsDifferent sessions, permissions and data per userA seeded set of test accounts, never real customers
Search termsDifferent result sizes and cache keysTerms from your own search logs, with sensitive ones removed
Product or item IDsSpread across the catalogue, not one hot itemAn export of IDs from the staging database
Order or cart contentsDifferent total sizes and stock checksGenerated within the rules of your catalogue
Dates and amountsDifferent ranges and report sizesGenerated

Use test accounts and data created for the purpose. Do not put real customer credentials or personal data into a script or a CSV file in a repository. The post on cloud testing challenges covers masking and why.

The size of the underlying data set matters as much as the test data. A database with 200 rows behaves very differently from one with 20 million, so seed the target environment with a realistic volume before measuring anything.

How do I load a CSV file in k6?

In k6, read the file once in the init stage with open(), parse it, and wrap the result in SharedArray, so all virtual users share one copy in memory. open() works only in the init context, not inside the default function. To make each virtual user take its own row, index the array by the virtual user’s ID from k6/execution.

import http from 'k6/http';
import { check, sleep } from 'k6';
import exec from 'k6/execution';
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';

const users = new SharedArray('users', function () {
  return papaparse.parse(open('./users.csv'), { header: true }).data;
});

export const options = { vus: 20, duration: '2m' };

export default function () {
  // each VU keeps its own row for the whole test
  const user = users[(exec.vu.idInTest - 1) % users.length];

  const res = http.post(
    'https://staging.example.com/api/login',
    JSON.stringify({ username: user.username, password: user.password }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  check(res, { 'login ok': (r) => r.status === 200 });
  sleep(1);
}

Choosing a random row, as many examples do (Math.floor(Math.random() * users.length)), is fine for search terms but not for accounts: two virtual users can pick the same account in the same second. With the VU ID, and at least as many rows as virtual users, no two users share an account. If you need a different row on every iteration instead, use exec.scenario.iterationInTest. The names are in the k6 execution context documentation.

How do I give every Locust user its own row?

In Locust, load the file once at module level and hand out rows from a shared iterator in on_start, which Locust calls once for each simulated user. Each user then keeps its row for its whole life.

import csv
import itertools
from locust import HttpUser, task, between

with open("users.csv", newline="") as f:
    rows = list(csv.DictReader(f))
next_row = itertools.cycle(rows).__next__

class Shopper(HttpUser):
    wait_time = between(1, 5)

    def on_start(self):
        self.account = next_row()
        self.client.post("/login", json={
            "username": self.account["username"],
            "password": self.account["password"],
        })

    @task(3)
    def browse(self):
        self.client.get("/products")

    @task(1)
    def view_profile(self):
        self.client.get("/profile")

Within one Locust process this hands out rows in turn. If you run several worker processes, each has its own iterator and rows will repeat across workers, so split the file per worker or add the worker’s index to the choice. The @task(3) and @task(1) weights make browsing three times as likely as viewing the profile, which is a first step towards a realistic mix.

How do I model a journey that branches?

A journey is a sequence of steps in which later requests depend on earlier responses, and where users do not all take the same path. Build it from these parts:

  • Passed values. Read the token, order ID or cart ID from one response and use it in the next request. In k6 that is res.json('token') or a regular expression on the body; in Locust, response.json(). A script that hard-codes a token from a recording will fail or test nothing.
  • Branches. Decide by data or by chance: a returning user skips registration, a cart with a coupon goes through a different path. Choose the branch with a weighted random draw that matches the proportions you see in analytics.
  • Loops. A user views several products before buying. Loop a data-driven number of times, not a fixed one.
  • Think time. Pause between steps. Without it, every virtual user acts as a machine and the request rate overshoots what the same number of people would produce.
  • Failure paths. Include the retry after a declined payment or an expired session if those paths exist in production.

Keep journeys small enough to read. A script of twelve branches that nobody can follow will not survive the next release. Model the journeys that carry the most traffic or the most risk first. The posts on designing load testing scenarios and simulating realistic user behaviour cover how to pick them and the proportions between them.

How do I handle data that can only be used once?

Some data is consumed by use: a one-time coupon, an invitation token, an email address that can only register once, a seat that can only be booked once. A test that reuses it will fail on the second iteration, and the errors will say nothing about performance.

Either generate fresh values per iteration, such as a unique email built from the VU ID and iteration number, or prepare one row per expected use and make sure the file is large enough for the whole test: virtual users times iterations per user. When the data runs out, decide what should happen. JMeter’s CSV Data Set Config can stop the thread or recycle the file, and k6 and Locust leave it to your code. Reset or recreate the target data between runs so run two starts as clean as run one.

What mistakes should I avoid?

  • Not enough rows. With 20 rows and 500 virtual users, accounts are shared and you are back to the fixed-user problem.
  • Perfectly even data. Real catalogues have hot items and long tails. If your logs show that 10% of products take most of the views, weight the data the same way.
  • Real credentials in the repository. Use test accounts and keep the file out of version control or encrypted.
  • Recorded tokens. They expire. Fetch them in the script.
  • Reading files in the iteration. Parsing a CSV on every iteration costs generator CPU and distorts timings. Load once in init.
  • Trusting a green run on one data set. Run again with a different seed, or a different slice of the data, to see whether the numbers hold.

How do I run data-driven scripts on LoadFocus?

LoadFocus runs JMeter and k6 scripts from 25+ cloud regions (loadfocus.com/index.md, October 2026). For k6, its documentation says to upload the script and its data files together, so that they sit in the same folder when the test runs, and to read them with open() in the init stage inside SharedArray (see uploading k6 scripts and data files). For JMeter, each engine can read its own CSV file: you upload files named config1.csv, config2.csv and so on, and the CSV Data Set Config filename uses the LF_SESSION_INDEX variable that LoadFocus sets for each engine (see using multiple CSV files with JMeter engines). This prevents two engines from reading the same rows. The setup of CSV Data Set Config itself is covered in how to use CSV Data Set to configure JMeter tests, so it is not repeated here. For the product pages, see k6 load testing and JMeter load testing.

Frequently Asked Questions

What is data-driven load testing?

It is load testing in which the inputs to each request, such as accounts, search terms and item IDs, come from a data set rather than being fixed in the script. Each virtual user sends different values, and the journey can branch depending on the data or the responses.

Why not just use random data?

Random values are fine for search terms, amounts and similar fields. For accounts and one-time values, random choice produces collisions. Assign rows by virtual user ID or from a shared iterator instead.

How many rows of test data do I need?

At least one per virtual user for accounts, so no two users share one. For one-time values, one per expected use: virtual users times the iterations each will run. For read-only items such as products, enough to cover the catalogue spread you see in production.

Should I use production data in a load test?

No. Use test accounts and generated or masked data. Copying production data into a test environment moves personal data to a place with weaker controls.

Should the same scripts run on every commit?

Run a short version, with a small data set, as a smoke test in the pipeline, and the full data-driven run on a schedule or before releases. The post on CI/CD best practices covers the pipeline side.

Sources

  • Grafana k6 documentation, SharedArray and execution context variables (grafana.com/docs/k6/latest, read October 2026).
  • Locust documentation, writing a locustfile: on_start, wait_time, task weights (docs.locust.io, read October 2026).
  • LoadFocus documentation: uploading k6 scripts and data files; using multiple CSV files with JMeter engines (loadfocus.com/docs, read October 2026).

Chris

Head of Content at LoadFocus

Chris leads content and oversees development across the services this blog covers. The guides and tool comparisons here come out of the same decisions that shape what ships.

Weekly tips right in your inbox.

Don’t miss out on the latest testing tips and tricks, tools and news related to software testing.

Already +20,000 members in the list.

The Load Testing Platform with AI That Explains Your Results

Load test websites and APIs from 25 cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.

Cloud load testing, website speed and API monitoring in one place.

Hi there! Can I share a cool thing I'm working on with you?

A software testing newsletter that sparks joy.

Our goal is to create helpful content for software developers and testers.

If you're not into it, you can unsubscribe instantly.