Module 6: Checks Groups And Realistic Scenarios

7. Realistic think time with jitter

Overview

You've used sleep() since module 2 as "think time": the pause that imitates a user thinking between actions. Without that pause, a VU fires requests as fast as the server can answer —an automatic hammer, not a person—. This lesson closes the module by refining that think time with two ideas that make it truly realistic. The first you already half-know: think time governs the pace, and without it the test measures a storm no real user would produce. The second is new and subtle: if all the VUs pause exactly the same, they synchronize and create artificial spikes —they march in military formation, all requesting at the same instant—, which isn't realistic either. The solution to the second is jitter: making the pause random within a range, so the VUs desynchronize the way real users desynchronize.

Jitter is a small idea with a big effect. Instead of an exact sleep(1), you pause a random time around a base value —for example, between 0.5 and 1.5 seconds, that is 1 second ± 50%—. Each VU, in each iteration, waits a different time. The result is that the requests spread out over time instead of piling up in spikes. Think of it: without jitter, if 50 VUs start at once and all pause exactly 1 second, they fire their requests in synchronized waves —50 at once, a second of silence, 50 at once—. With jitter, those 50 requests spread out over the second, a continuous trickle, like real traffic. In this lesson you see measured how think time governs the pace (with vs without) and how jitter spreads the pauses.

Connection to the module: think time with jitter is the last piece of the realistic scenario, the touch that gives it a human rhythm. In k6 it's written with sleep(Math.random() * ...) (content); in Python, with time.sleep() over a random value (executed). You'll see two things measured in this environment with Python 3.14.0: the same quote → book → confirm scenario running with no pause (a hammer) vs with realistic think time (a human rhythm), and the distribution of the jittered pauses. The boundary: here think time modulates a single VU's rhythm; how the number of VUs rises and falls over time is the load profiles of module 4.

The badly synchronized traffic lights

Think of the traffic on an avenue with several traffic lights. If all the lights turn green at exactly the same time, something bad happens: all the cars start at once, advance in a tight pack to the next light, all stop together, and all start together again. The traffic moves in waves: moments of total saturation followed by empty streets. It's the worst of worlds —neither smooth nor even—. Traffic engineers know this, which is why they desynchronize the lights on purpose: they set them to change with small offsets, so the cars spread out along the avenue in a continuous flow instead of in packs.

VUs with no jitter are the synchronized lights. If 50 VUs start together and all pause an exact sleep(1), their requests come out in packs: 50 at once, silence, 50 at once. The server sees artificial waves of saturation no real traffic produces. Jitter is desynchronizing the lights: each VU pauses a slightly different time (random within a range), so its requests spread out in a continuous trickle, like the real traffic of users who aren't coordinated with each other. A user doesn't check their watch to request in the same millisecond as another; each goes at their own pace. Jitter reproduces that lack of coordination.

Think time governs a VU's pace: without it, it fires requests like a hammer, not like a person. And if all the VUs pause exactly the same, they synchronize into artificial spikes, like traffic lights that all change at once. Jitter —making the pause random within a range— desynchronizes them, spreading the requests into a continuous flow like the real traffic of uncoordinated users.

Think time with jitter in k6 (content)

In k6, think time is sleep(seconds), and the jitter is achieved by passing it a random value instead of a constant. Math.random() gives a number between 0 and 1; with a little arithmetic you turn it into a range:

// think_time_jitter.js - think time with jitter between steps.
// SHOWN AS CONTENT: k6 is not installed in this environment.
import http from 'k6/http';
import { check, group, sleep } from 'k6';

const BASE_URL = 'http://localhost:8000';
const params = { headers: { 'Content-Type': 'application/json' } };

// Random pause in [min, max] seconds: base +/- jitter.
function thinkTime(min, max) {
  return Math.random() * (max - min) + min;   // e.g. thinkTime(0.5, 1.5)
}

export default function () {
  group('quote', function () {
    const body = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
    const res = http.post(`${BASE_URL}/quote`, body, params);
    check(res, { 'status is 200': (r) => r.status === 200 });
  });

  sleep(thinkTime(0.5, 1.5));   // JITTERED think time: 1s +/- 50%, different per VU/iteration

  group('book', function () {
    const body = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
    const res = http.post(`${BASE_URL}/book`, body, params);
    check(res, { 'status is 200': (r) => r.status === 200 });
  });

  sleep(thinkTime(0.5, 1.5));   // another jittered pause before the next iteration
}

Take it apart:

  • Math.random() * (max - min) + min. The standard jitter formula. Math.random() gives [0, 1); multiplying it by (max - min) stretches it to the width of the range; adding min shifts it to the start of the range. thinkTime(0.5, 1.5) gives a random number between 0.5 and 1.5 seconds —a one-second base with ±50% jitter—.
  • sleep(thinkTime(0.5, 1.5)) between steps. Each time it runs, the number is different, so two VUs (or two iterations of the same VU) almost never pause the same. That's what desynchronizes them.
  • The pause goes between groups, not inside (as you saw in lesson 4): the think time is the user's thinking time, not the step's execution time, and it must not inflate the group_duration.

A note on magnitudes: 0.51.5 s is an example. The real think time depends on what the user is doing —reading a list of rooms can take several seconds; confirming a click, a fraction—. What doesn't change is the technique: a pause with jitter, centered on a realistic value for that action.

The effect of think time, measured (executed)

Let's first see why think time matters so much, with the module's most compelling measurement. Let's run the same quote → book → confirm scenario two ways, with the same load (3 VUs / 3 s): one with no pause (think 0, the hammer) and another with realistic think time with jitter (think 200 ± 50%).

What to expect. With no pause, each VU fires iterations as fast as it can: a great many per second. With think time, each VU waits ~200 ms between iterations, lowering the pace drastically. Real output in this environment:

--- think 0 (no pause: the hammer) ---
  vus............: 3
  real duration..: 3.00s
  iterations.....: 6477   (2158.6/s)
  checks.........: 100.00%   (51816 of 51816)
  ...
  per group (avg latency, n calls):
    group 'quote  '.....: n=6477 avg=0.47ms
    group 'book   '.....: n=6477 avg=0.47ms
    group 'confirm'.....: n=6477 avg=0.44ms

--- think 200 (with jitter +/-50%: realistic pace) ---
  vus............: 3
  real duration..: 3.25s
  iterations.....: 43   (13.2/s)
  checks.........: 100.00%   (344 of 344)
  ...
  per group (avg latency, n calls):
    group 'quote  '.....: n=43   avg=1.52ms
    group 'book   '.....: n=43   avg=0.65ms
    group 'confirm'.....: n=43   avg=0.54ms

The difference is enormous and must be read carefully:

  • think 0: 6477 iterations, 2158.6/s. With no pause, 3 VUs produced 6477 complete run-throughs in 3 seconds —over 2000 iterations per second—. That's a hammer: three "users" firing the three-step flow at maximum speed, without stopping to think. No human does that. With just 3 VUs, you generated a load that in real users would require a great many more.
  • think 200: 43 iterations, 13.2/s. With ~200 ms think time between steps, the same 3 VUs produced 43 iterations —13 per second—. Each VU spends most of the time waiting (thinking), like a real user. The pace dropped ~160 times compared to the hammer.
  • The think-time lesson. The same number of VUs produces radically different loads according to the think time. That's why 3 VUs don't mean "3 users/second": they mean 3 concurrent users, and how many requests/second they generate depends on how much they think between actions. A realistic think time is what turns "N VUs" into "a credible request rate." Without it, you brutally overestimate the load N users produce (it's Little's law you saw in module 3, now with the complete flow).

Notice an honest detail: with no pause (think 0), the per-group latency dropped (quote 0.47ms vs 1.52ms with think). It's not that the hammer is "more efficient": it's that at 2158 iterations/s the server is in a different regime, and the latency averages are measured over a huge volume of identical, warm requests. It's another face of the same problem: without realistic think time, all the numbers come out distorted.

Jitter spreads the pauses, measured (executed)

Now let's see what jitter does with the pauses. We look at the distribution of the jittered pauses of a 200 ms base think time with ±50%, over 1000 samples:

# The shape of the jitter: base pause +/- 50%, random each time.
def jittered(base_ms, rng):
    return base_ms * rng.uniform(0.5, 1.5)      # base +/- 50%

What to expect. The pauses should spread between 100 ms (base × 0.5) and 300 ms (base × 1.5), with an average near 200 ms, and not concentrate on a single value. Real output in this environment:

  think base=200ms, jitter +/-50%, 1000 samples:
    min=100.0ms  avg=196.3ms  max=299.8ms
    split by third of the range: [370, 316, 314]  (all different, not in a line)

Read it:

  • min=100.0ms, max=299.8ms — the pauses go from 100 to 300 ms, exactly the range 200 ± 50%. No pause falls outside; the jitter is bounded.
  • avg=196.3ms — the average is near the 200 ms base (not exactly, because 1000 samples is a finite sample). On average, the think time is still ~200 ms; the jitter doesn't change the center, it only spreads it.
  • split by third of the range: [370, 316, 314] — of the 1000 pauses, 370 fell in the low third (100–167 ms), 316 in the middle, 314 in the high one. Spread across the whole range, not piled up at one value. This is what desynchronizes the VUs: since each one waits a different time (100, 167, 234, 289… ms), its requests don't come out in a pack but spread out.

Contrast it with a think time without jitter (sleep(0.2) exact): the 1000 pauses would all be 200 ms, and VUs that started together would stay together forever, firing in waves. Jitter breaks that formation. The average is the same (~200 ms), but the spread is what gives it realism: uncoordinated users, a continuous trickle instead of packs.

Common mistakes

Running the test with no think time and thinking N VUs = N users. What happens: someone runs 10 VUs with no sleep and reports "I tested with 10 users." Why it happens: a VU (which hammers with no pause) gets confused with a user (who thinks between actions). How to detect it: an absurdly high iterations/s rate (thousands/s with few VUs), like the think 0 at 2158/s. How to fix it: put a realistic think time between steps. Without it, your N VUs generate the load of hundreds of real users, and you brutally overestimate what your system holds "per user."

Using the same exact sleep in all VUs. What happens: someone puts a fixed sleep(1), and under many VUs the server sees synchronized waves of requests (artificial spikes). Why it happens: a constant value is the first thing one writes. How to detect it: pack-load patterns —periodic bursts followed by silences— that don't resemble real traffic. How to fix it: add jitter (sleep(thinkTime(0.5, 1.5))), a random pause within a range, to desynchronize the VUs. The traffic lights go offset, not all green at once.

Putting the think time inside the group() (again). What happens: someone puts the sleep inside a step's block, and that step's group_duration comes out inflated by the pause. Why it happens: "the whole step" gets bundled into a block. How to detect it: a group with latency ≈ the think-time value. How to fix it: the think time goes between groups. It's the user's thinking time, not the step's execution; it must not count in the step's measurement. (You already saw it in lesson 4; with jitter it applies the same.)

Exercises

Exercise 1 — Write a think time with jitter. Write (in k6, as content) a thinkTime(min, max) function that returns a random pause in seconds, and use it for a think time of 2 seconds ± 50% (that is, between 1 and 3 seconds). What sleep call would you put?

See solution
function thinkTime(min, max) {
  return Math.random() * (max - min) + min;
}

// 2 seconds +/- 50% = range [1, 3]:
sleep(thinkTime(1, 3));

Math.random() * (3 - 1) + 1 gives a number in [1, 3). The base is 2 s (the center of the range) and the jitter is ±1 s (±50%).

Exercise 2 — Read the effect of think time. In the real runs, think 0 gave 6477 iterations (2158.6/s) and think 200 gave 43 (13.2/s), both with 3 VUs. (a) Why does the same number of VUs produce such different rates? (b) If you wanted 3 VUs to generate ~30 iterations/s, would you raise or lower the think time relative to 200 ms? (c) What does this mean for interpreting "I tested with 3 VUs"?

See solution
  • (a) Because the think time governs how long each VU waits between iterations. With no pause (think 0), each VU iterates as fast as the server responds (sub-millisecond → thousands/s). With think 200, each VU waits ~200 ms per iteration, so 3 VUs give ~15/s. The rate depends on the think time, not just the VUs.
  • (b) You'd lower the think time. With a smaller pause, each VU iterates more often, so the rate rises. With 3 VUs and ~200 ms you get ~13/s; for ~30/s you'd need shorter pauses (on the order of ~100 ms), or more VUs.
  • (c) That "3 VUs" doesn't say how many requests/s are generated: it says how many concurrent users there are. The real rate depends on the think time. Reporting a load test requires stating VUs and think time (or directly the iterations/s rate), never just the VUs.

Exercise 3 — The traffic lights. A colleague runs 100 VUs with an exact sleep(1) and sees the server receive the requests in waves: bursts every second, with silences in between. (a) Explain why it happens, with the traffic-lights analogy. (b) What one-line change would fix it? (c) Would that change the average think time?

See solution
  • (a) Their 100 VUs are synchronized traffic lights: they started together and all pause exactly 1 second, so they fire their requests at the same instant, wait 1 second all together, and fire together again. The result is waves (100 requests at once, silence, 100 at once), an artificial pattern real traffic doesn't have.
  • (b) Change sleep(1) to a pause with jitter, for example sleep(thinkTime(0.5, 1.5)) (1 s ± 50%). Each VU would wait a different time, desynchronizing them, and the requests would spread into a continuous trickle.
  • (c) No (or almost none). Jitter keeps the think time centered on ~1 s on average; it only spreads it. As seen in the measurement, base 200 ms with jitter gave avg=196.3 ms —practically the base—. Jitter changes the spread of the pauses, not their center.

Summary and next step

In this lesson you put the last touch of realism: think time with jitter. Think time governs a VU's pace —you saw it measured: the same scenario with 3 VUs produced 2158 iterations/s with no pause (a hammer) against 13/s with realistic think time, a difference of ~160×—, which proves that "N VUs" doesn't say the load without also saying how much they think. And jitter —making the pause random within a range (base ± 50%)— desynchronizes the VUs so they don't fire in packs, like offset traffic lights: the real distribution of a 200 ms think time showed pauses spread from 100 to 300 ms (avg 196.3 ms), spread across the whole range instead of piled up. The average doesn't change; the spread does, and that's what gives the continuous trickle of real traffic.

Before moving on you should be able to: write a think time with jitter (Math.random() * (max - min) + min); explain why without think time N VUs overestimate the load; explain why a fixed sleep synchronizes the VUs into artificial spikes and jitter spreads them; and report a test with VUs and think time, never just VUs.

With this you have the four pieces of the realistic scenario —correctness checks, groups, parametrization, correlation— plus think time with jitter. Lesson 8 brings them all together in the mini-project: run the parametrized quote → book → confirm scenario against the canonical API, with checks at each step and the booking_id correlated, reporting the checks rate and the per-group metrics. It's your graduation from the module: the complete scenario piece, executed, ready to analyze and take to CI in module 7.

Resources