Module 2: The K6 Script And Virtual Users
5. `sleep()` and think time
Overview
There's a line that has appeared in almost every script in this module and that we've used without stopping: sleep(1). It looks trivial —"wait one second"— but it's one of the most important decisions of a load test. That pause is the think time: the time a human takes between one action and the next —reading the screen, deciding, moving the mouse—. Without it, your virtual user stops resembling a person and becomes a jackhammer that hits the API thousands of times per second. And a test that measures a jackhammer doesn't tell you how your system will behave with real users.
The difference is huge and measurable. The same VU, without sleep, can do thousands of iterations in a few seconds; with a sleep(1), it does one per second. It's not a cosmetic tweak: it completely changes the load your test imposes and, therefore, the conclusions you draw. In this lesson we understand what think time is, why you almost always want it, when you don't want it, and we confirm it with the same VU running with and without the pause —seeing how sleep governs the loop's pace—.
Connection to the module: in lesson 2 you saw that the VU loop is open —it goes as fast as each lap lasts— and that removing the sleep spiked the iterations. This lesson explains why that matters and how to use sleep to model realistic load. k6's sleep() is shown as content; Python's time.sleep(), its exact equivalent, is actually run in the generator, and you'll see the same VU do 13,598 laps with no pause and 6 with sleep(0.5). Everything labeled as Python output was measured in this environment with Python 3.14.0. The load's pace is tuned here; the load profiles (raising and lowering VUs over time) are module 4.
The customer who reads the menu vs. the one who shouts orders
Think of it this way. Go back to the restaurant from lesson 3. A real customer calls, asks about the menu, takes a few seconds to think, says their order, hangs up; a while later they call again to order dessert. Between each action there are human pauses: they read, decide, speak. The kitchen, with a hundred customers like this, has a manageable flow —the calls arrive spaced out—.
Now imagine a customer who, as soon as they hang up, dials again instantly, with no pause, shouting orders as fast as the phone allows: a thousand orders per minute, from just one of them. That customer doesn't exist in real life, but it's exactly what a VU does without think time: it repeats the script at maximum speed, without the pauses a human would have. If you test your kitchen with customers like that, you measure something that will never happen —an unreal avalanche— and draw wrong conclusions: you'll think your system handles far fewer "users" than it actually would, because each jackhammer-VU weighs like dozens of real people.
Think time (
sleep) is the human pause between actions. Without it, a VU repeats the script at maximum speed —a jackhammer, not a person— and your test measures an unreal avalanche. With it, each VU resembles a real user, and the load you impose reflects what will happen in production.
sleep() in k6 (content)
sleep is imported from 'k6' and receives a number of seconds (can be a decimal). It goes where a human would pause: usually at the end of the script, before the next iteration starts, and sometimes between steps of a flow:
// quote_sleep.js — quote with realistic think time.
// SHOWN AS CONTENT: k6 is not installed in this environment.
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = 'http://localhost:8000';
export default function () {
const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post(`${BASE_URL}/quote`, payload, params);
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(1); // think time: 1 second before the next iteration.
}
Three details:
sleep(seconds). The argument is seconds;sleep(1)is one second,sleep(0.5)half a second,sleep(3)three. It's pure wait time: the VU does nothing, doesn't consume the API, it just lets the clock tick.- Where it goes. The typical spot is a
sleepat the end of the script, separating one iteration from the next (like the customer who waits before calling again). In multi-step flows, you can put pauses between steps —quote,sleep, book— to imitate a user who thinks between clicks. - It doesn't count as request time. The
sleeplengthens the iteration, but not the request. In module 3 you'll see that k6 reportshttp_req_duration(the request time) separately fromiteration_duration(the full lap, which does include thesleep). Think time doesn't pollute your latency metrics; it only spaces out the load.
How much think time: realistic values
One second, two, half? It depends on what you're modeling. The idea is that the think time reflects the real pace of your users:
- A user browsing (reading, deciding between screens) has pauses of several seconds —
sleep(3)tosleep(10)isn't unusual—. - A user in a fast flow (quoting and booking one after another) has short pauses, of one or two seconds.
- An automated client or a machine-to-machine integration (another service calling your API) can have little or no think time —there you do want to model something close to a hammer, because that is the real thing—.
A common practice is to vary the think time a bit (for example, a random value between 1 and 3 seconds) so the VUs don't all hit at the same time, like real users who aren't synchronized. In this module we use fixed values so the numbers are clear; realistic randomness is a refinement you'll see in module 6's scenarios. The essential thing now: think time is a modeling decision, not a technical detail. Choose it thinking "how often does one of my users really act?".
The same VU, with and without think time (executed)
Here's the demonstration that makes all of the above tangible. We take the same VU —a Python thread— running the same duration (3 seconds) against the same API, and only change one thing: whether there's a sleep.
First, without think time. The VU hits /quote as fast as it can:
What to expect. With no pause, each lap lasts only as long as the request takes (~0.2 ms on localhost), so the VU will do thousands of iterations. Real output (1 VU, 3 s, no think time):
----------------------------------------------------------
Python load generator -> 1 VUs / 3s / no think time
----------------------------------------------------------
vus............: 1
real duration..: 3.00s
iterations.....: 13598 (4532.5/s)
checks.........: 100.00% (27196 of 27196)
http_errors....: 0
req_duration...: avg=0.20ms min=0.14ms max=6.27ms
----------------------------------------------------------
Now the same VU, the same duration, but with sleep(0.5) —half a second of think time— after each quote:
What to expect. With half a second of pause per lap, each iteration lasts ~0.5 s, so in 3 seconds there's room for about 6. Real output (1 VU, 3 s, 500 ms think time):
----------------------------------------------------------
Python load generator -> 1 VUs / 3s / think 500ms
----------------------------------------------------------
vus............: 1
real duration..: 3.06s
iterations.....: 6 (2.0/s)
checks.........: 100.00% (12 of 12)
http_errors....: 0
req_duration...: avg=2.20ms min=0.73ms max=6.27ms
----------------------------------------------------------
Compare the two, because the contrast is the whole lesson:
- Same VU (1), same duration (3 s), opposite results: 13,598 iterations vs 6. The only thing that changed was the
sleep(0.5). Think time governs the loop's pace: without it, the VU goes to the machine's limit; with it, it goes at the pace you impose. iterations/s: 4532.5/s with no pause, 2.0/s withsleep(0.5). Half a second of pause per lap gives ~2 laps per second —exactly what the arithmetic1 / 0.5 = 2dictates—. The pace is predictable and yours.- The real load you impose is vastly different. A single VU with no think time weighs, on the server, like thousands of requests per second —the equivalent of hundreds of real users crammed into one—. With
sleep(0.5), that same VU weighs like... one person acting twice a second. If you size your test with jackhammer-VUs, you'll think your system collapses with "10 users," when in reality those 10 pauseless VUs were equivalent to thousands of people. req_durationbarely changed (0.20 ms vs 2.20 ms, both very fast): thesleepdoes not inflate the request latency. It lengthens the iteration, not the request. The slight difference here is measurement noise, not the effect of think time.
That's the reason think time almost always goes in your scripts: without it, you're not measuring your users, you're measuring how fast your machine can fire a loop.
When you don't want think time
Think time models human users, but not everything that hits your API is human. There are legitimate cases where you remove it or reduce it:
- Stress tests at the limit (module 1): sometimes you want, on purpose, to see how many requests per second the API holds before breaking, without the courtesy of think time. There the goal isn't user realism, but finding the ceiling.
- Machine-to-machine integrations: if your API is consumed by another service in a tight loop (with no human in between), the realistic thing is little or no think time —that's what will really happen—.
- Arrival-rate load models: there's a way to generate load where you don't control each VU's think time but the rate of requests per second directly (the
constant-arrival-rateexecutors). That changes the role of thesleepand is a module 4 topic.
The rule isn't "always add sleep," but "model the real pace of whoever uses your API." For human users, that almost always means think time; for machines or limit tests, maybe not. What you must never do is put in jackhammer-VUs without realizing it and then interpret the results as if they were real users.
Common mistakes
Forgetting the sleep and measuring an unreal avalanche. What happens: someone writes a script with no sleep, runs "10 VUs," sees the API saturate, and concludes "my system doesn't even hold 10 users." Why it happens: they didn't add think time, so each VU hit at maximum speed —thousands of requests per second—. How to detect it: the iterations/s is huge (thousands) for few VUs, and the load looks like nothing human. How to fix it: add sleep(1) (or your users' realistic think time) at the end of the script. Ten VUs with a pause resemble ten people; ten VUs with no pause resemble thousands.
Passing milliseconds instead of seconds to sleep. What happens: someone comes from APIs where times are in milliseconds and writes sleep(500) meaning half a second. Why it happens: they confuse the unit. How to detect it: each iteration takes 500 seconds (over 8 minutes), the test seems hung and does almost no iterations. How to fix it: sleep receives seconds; half a second is sleep(0.5), not sleep(500). (In Python's time.sleep it's the same: seconds.)
Believing the sleep inflates the reported latency. What happens: someone is afraid to add think time because "it'll make my response times look sky-high." Why it happens: they confuse the iteration time with the request time. How to detect it: check that http_req_duration (request) and iteration_duration (full lap) are different metrics; the sleep only affects the second. How to fix it: add the think time without fear: it doesn't pollute http_req_duration. The request latency is measured separately from the time the VU spends waiting.
Exercises
Exercise 1 — Estimate the iterations. A VU runs a script with a ~2 ms request and a sleep(0.25) at the end (a quarter second). If the run lasts 4 seconds with 1 VU, how many approximate iterations do you expect? And with 2 VUs?
See solution
With 1 VU: each lap lasts ~0.25 s (the 2 ms request is negligible). In 4 seconds there's room for 4 / 0.25 = ~16 iterations. The pace is ~4 iterations per second (1 / 0.25).
With 2 VUs: each VU does its ~16, so in total ~32 iterations (the VUs run in parallel, so they add up). The duration per lap doesn't change; what doubles is the number of actors.
Exercise 2 — Diagnose the wrong conclusion. A colleague ran "5 VUs with no sleep" against Reservo, saw 20,000 iterations in 4 seconds and the API started giving errors. They concluded: "Reservo doesn't even hold 5 users." Why is their conclusion misleading and what should they change?
See solution
Their conclusion is misleading because 5 VUs with no think time aren't 5 users: they're 5 hammers hitting at maximum speed, about 5000 iterations per second in total —the equivalent of thousands of real people, not 5—. The API didn't fail "with 5 users"; it failed with an avalanche of thousands of requests per second that no group of 5 humans would ever generate.
What to change: add realistic think time (sleep(1) or similar). With sleep(1), 5 VUs would do ~5 iterations per second —that does resemble 5 users—, and only then could they conclude something about how many real users Reservo holds. Without think time, they only measured the requests-per-second ceiling, which is a different question (a stress test, not a realistic load test).
Exercise 3 — Choose the think time. For each scenario, propose a reasonable think time and justify it: (a) users browsing the room catalog, reading descriptions before quoting; (b) an automated booking service that syncs availability by calling your API in a loop; (c) a stress test to find the maximum requests per second /quote holds.
See solution
- (a) A long think time,
sleep(3)tosleep(8)(or random in that range): a human reading descriptions takes several seconds between actions. Modeling short pauses would exaggerate the load. - (b) Little or no think time (
sleep(0)or very short): it's a machine, not a human, and the realistic thing is that it calls in a tight loop. Here the "hammer" is the true behavior. - (c) No think time, on purpose: the goal of a stress test is to find the requests-per-second ceiling, so you want the VUs to hit at the limit. (In module 4 you'll see there are executors designed precisely to control the arrival rate in these cases.)
Summary and next step
In this lesson you understood the line we'd used without explaining: sleep() is the think time, the human pause between actions. Without it, a VU repeats the script at maximum speed —a jackhammer that weighs like thousands of users— and your test measures an unreal avalanche; with it, each VU resembles a person and the load reflects what will happen in production. You measured it with the same VU: 13,598 iterations in 3 s with no pause vs 6 with sleep(0.5) —the proof that think time governs the loop's pace—, and you saw that sleep lengthens the iteration but not the request latency. You also saw when you don't want think time: stress tests at the limit and machine-to-machine integrations.
Before moving on you should be able to: add sleep with the correct value (seconds) in the correct place in a script; estimate how think time changes the number of iterations; choose a realistic think time according to who uses your API; and explain why a VU with no pause doesn't represent a user.
You now have the four pieces of the script: the request, the check, the think time, and the loop. The cast sheet is missing. Lesson 6 opens options: vus and duration —how many virtual users run and for how long—, the configuration that, separate from the script, decides your test's scale.
Resources
sleep()in k6 — the function's reference: it receives seconds and pauses the VU. The exact source of this lesson.- How to generate realistic load with k6 — why think time makes your VUs resemble real users, and what happens without it. The foundation of pace modeling.
time.sleep— Python documentation — the exact equivalent in Python (seconds) we run the generator's think time with.- Google SRE — Latency and user behavior — why modeling users' real pace matters for measuring the load your system will actually face. The "why" behind think time.