Skip to main content

How to Simulate Realistic User Behavior in Load Testing Scenarios (2026 Guide)

How to Simulate Realistic User Behavior in Load Testing Scenarios (2026 Guide)

Key takeaways

  • Real users pause, backtrack and abandon; a test without think time creates load nobody generates.
  • Mix journeys by real proportion rather than testing each endpoint evenly.
  • Vary data per user, or caching makes the system look faster than it is.
  • Decide whether you are modelling a fixed number of users (closed model) or a rate of arrivals (open model), because they behave differently when the system slows down.

To simulate realistic user behavior in a load test, give each virtual user a journey, a pace and its own data, then decide how new users arrive. Journeys come from your analytics, the pace from measured pauses between actions, and the arrival pattern from your traffic. This guide covers those modelling details. For the overall method (goals, workload model, ramp-up and pass/fail criteria) read how to design effective load testing scenarios first.

Why Do Uniform Load Tests Miss Real Problems?

A uniform test sends the same request sequence at a fixed pace from identical users. It passes easily because it exercises one warm code path. Production traffic differs in four ways that matter to a server: requests arrive in bursts, users pause for different lengths of time, a minority of users do expensive things (search, export, checkout), and each user touches different data. A test that ignores these can report fast, stable response times for a system that struggles on a normal Tuesday.

You do not need a statistic to justify the effort. The reasoning is mechanical: a cache only helps repeated requests, a connection pool only fills when sessions overlap, and a lock only contends when two users want the same row. A uniform script rarely triggers any of the three.

How Do You Build User Journeys From Real Data?

Start from what people actually do. Pull the top paths from your analytics tool (entry page, pages per session, exit page), and the request counts per endpoint from your server or CDN logs. Group sessions into a handful of journeys, usually three to six, such as “browse and leave”, “search then view”, “log in then use a feature”, “checkout”. Write down for each journey its share of sessions and its typical number of steps.

Keep the privacy rule simple: use aggregated counts and shapes, not individual user records. If you need realistic values in requests, generate synthetic ones.

JourneyShare of sessionsTypical stepsCost to the backend
Browse and leave60%3 pagesMostly cacheable reads
Search, then view25%5 requestsSearch index, database reads
Log in and use a feature10%10 to 20 requestsAuthentication, writes
Checkout5%8 requestsPayment provider, database writes

The shares above are an illustration, not a benchmark; replace them with your own numbers. The point is that the cheap journey carries most of the sessions while the expensive one decides your capacity, so a test that splits users evenly overstates checkout load and understates cache pressure from browsing.

How Long Should Virtual Users Pause Between Actions?

A person reading a page, filling in a form or comparing two products pauses for seconds to minutes. This pause is called think time, and removing it makes each virtual user send far more requests per minute than a person would. Fixed think times are better than none but still produce lockstep behavior, with every user firing in the same instant.

Real pauses are skewed: many short ones and a few long ones. A random timer gives a closer match than a constant. In JMeter, the Gaussian Random Timer and Uniform Random Timer add a random delay on top of a constant offset (see the JMeter component reference); the Throughput Controller can run a branch for only a given percentage of iterations, which is a convenient way to apply the journey shares above. In k6, call sleep() with a random value.

The best source is your own data: measure the time between consecutive requests in a session from your logs, look at the median and the long tail, and sample from that range. If you cannot measure it, say so in your test notes and use a modest random range such as 3 to 10 seconds for page-style flows, then check the resulting requests per user per minute against production.

Should You Model Fixed Users or an Arrival Rate?

This choice changes what the test measures. In a closed model a fixed number of virtual users loop: each starts its next iteration only when the previous one finishes. When the system slows down, users take longer per iteration, so the test sends fewer requests exactly when the system is struggling. In an open model, new users arrive at a set rate no matter how slow responses get, which is how a public website behaves. The k6 documentation describes both and notes that the closed model can hide problems because slow responses reduce the load (grafana.com/docs/k6, open and closed models, read October 2026).

Use an open model (arrival rate) for public sites and APIs where traffic does not wait for you. Use a closed model for systems with a bounded user population, such as an internal tool with 200 employees. In k6 the constant-arrival-rate and ramping-arrival-rate executors implement the open model. In JMeter you approximate it with a Constant Throughput Timer or a Precise Throughput Timer, because threads are closed by nature.

How many virtual users does an arrival rate mean?

Little’s Law links the two: concurrent users equal arrival rate multiplied by the average time a user stays in the system. If 600 sessions start per hour (one every 6 seconds) and a session lasts 4 minutes, the average concurrency is 600 / 3600 x 240 = 40 users. Use this to size the test from analytics, and read more in how to calculate concurrent users.

What Does a Weighted, Randomized Scenario Look Like in k6?

The script below runs two journeys as separate open-model scenarios, with arrival rates in the 90 to 10 ratio of the sessions and a randomized think time. The URL is a placeholder and the numbers are for illustration.

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    browse: {
      executor: 'constant-arrival-rate',
      rate: 9, timeUnit: '1s', duration: '10m',
      preAllocatedVUs: 50, maxVUs: 200,
      exec: 'browse',
    },
    checkout: {
      executor: 'constant-arrival-rate',
      rate: 1, timeUnit: '1s', duration: '10m',
      preAllocatedVUs: 20, maxVUs: 100,
      exec: 'checkout',
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<800'],
  },
};

// exponential think time, mean about 5 s, capped at 30 s
function thinkTime() {
  return Math.min(30, -Math.log(1 - Math.random()) * 5);
}

export function browse() {
  const res = http.get('https://example.com/products');
  check(res, { 'status is 200': (r) => r.status === 200 });
  sleep(thinkTime());
  http.get(`https://example.com/products/${Math.ceil(Math.random() * 1000)}`);
  sleep(thinkTime());
}

export function checkout() {
  http.post('https://example.com/cart', JSON.stringify({ sku: Math.ceil(Math.random() * 1000) }),
    { headers: { 'Content-Type': 'application/json' } });
  sleep(thinkTime());
  http.post('https://example.com/checkout', null);
}

If you already have a JMeter or k6 script, you can upload it to LoadFocus JMeter load testing or k6 load testing and run it from 25+ cloud regions, so the traffic comes from where your users are.

Why Does Each Virtual User Need Its Own Data?

If every virtual user logs in as the same account, requests the same product and searches for the same term, the application serves everything from cache and the database sees almost nothing. Results look excellent and mean little. Give each user a different account, vary search terms and product ids, and draw ids from a realistic spread: a few popular items and a long tail of rare ones, not a uniform pick. The post on data-driven load testing scripts for complex user journeys shows how to feed such data in.

Also keep correlation in mind: values like session tokens and cart ids must be extracted from each response and reused, or your test sends requests no real client would.

Which Failures and Edge Behavior Should You Add?

Add behavior only when it reflects something you have observed:

  • Abandonment: a share of sessions stops partway through a journey (a cart left behind).
  • Retries: a user who gets an error or a slow page reloads. Retry traffic is a common cause of an incident turning from slow into down, so test it with a retry on a share of failures.
  • Bursts: a campaign email or a launch brings many arrivals within minutes. Use a spike stage rather than a gentle ramp.
  • Cold and warm caches: run once after a deploy or cache flush, and again warm, and compare.

Keep the first version small. Start with the two or three journeys that carry most of the load, confirm the result is stable, and add variation one change at a time. If everything is random from the first run, a failure is hard to reproduce. Record your assumptions (journey shares, think time range, data size) with the results so the next test can be compared. Our post on common performance testing mistakes covers other traps.

How Do You Check That the Test Looks Like Production?

Compare, after a run, the numbers the test produced with the numbers production shows for the same hour: requests per second per endpoint, the share of each endpoint in total requests, average requests per session and response sizes. If the test sends 3 times more requests per user than production, your think time is too short. If one endpoint is a much larger share than in production, your journey mix is off. Fix the model before you trust the response times.

Running monitoring checks during the test also helps. Synthetic traffic from a different tool shows what a single real journey experiences while the load runs.

Frequently Asked Questions

How many virtual users should I simulate for a realistic load test?

Derive it from traffic, not from a round number. Take your peak arrival rate of sessions, multiply by average session duration in seconds to get average concurrent users (Little’s Law), then test at that level, at 1.5 to 2 times it, and beyond to find the limit.

What is think time in load testing?

Think time is the pause a virtual user takes between actions to represent a person reading or deciding. Without it, each virtual user sends requests as fast as the server answers, which creates more load per user than any person would.

What is the difference between an open and a closed workload model?

In a closed model a fixed set of users loops, and each starts a new iteration after finishing the previous one. In an open model users arrive at a set rate regardless of how fast the system answers. Public websites behave like the open model.

How realistic should generated test data be?

Realistic in variety and distribution, not in content. Use unique accounts and different ids and search terms so caches behave as in production, and generate synthetic values instead of copying personal data.

Do I need AI tools to simulate realistic behavior?

No. A script with weighted journeys, random think times and varied data covers most of it. Tools that analyze logs can help you find the journeys and timings, but you still have to check the generated model against production numbers.

Sources

Published with PostNext

Bogdan

Founder at LoadFocus

Bogdan builds and runs the tools this blog is about. He writes from what the products actually do in production, including the parts that break.

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.