Session 2 · Users, tasks and wait times#

The big idea#

A Locust test is made of users. A user is a Python class; its methods marked @task are the things it does. Each user repeatedly picks a task, runs it, and then waits a little before the next one, like a real person who reads a page before clicking. You control how often each task is picked (its weight) and how long users wait.

Everyday example: shoppers in a mall. Most people window-shop (browse) and a few actually buy. They do not click every millisecond; they pause between actions. A test where every user hammers the server with no pause is not a mall, it is a stampede.

What one simulated user does A user picks a task, runs it, waits for the wait time, then picks the next task. Tasks with a bigger weight are picked more often. pick a task by weight run it e.g. GET /products wait between(1, 3) repeat until the test stops @task(3) browse · @task(1) buy → browse about 3 times for every buy
A user picks a task, runs it, waits for the wait time, then picks the next task. Tasks with a bigger weight are picked more often.

Tasks and weights#

@task marks a method as a task. @task(3) gives it weight 3, so it is picked three times as often as a task with weight 1. This locustfile has no HTTP in it; it just counts which task ran.

from collections import Counter

from locust import User, between, events, task

counts = Counter()


class Visitor(User):
    wait_time = between(0.1, 0.3)

    @task(3)
    def browse(self):
        counts["browse"] += 1

    @task(1)
    def buy(self):
        counts["buy"] += 1


@events.test_stop.add_listener
def show_counts(environment, **kwargs):
    total = sum(counts.values())
    print(f"tasks run: {dict(counts)}  (browse is {counts['browse'] / total:.0%} of {total})")

Run it headless with 4 users for 6 seconds:

locust -f 02_tasks.py --headless -u 4 -r 4 -t 6s --only-summary --loglevel WARNING

-u is the number of users, -r how many to start per second, -t the run time. The line it printed:

tasks run: {'browse': 87, 'buy': 30}  (browse is 74% of 117)

browse was picked about three times as often as buy (74% against a target of 75%). The exact counts change from run to run because the choice is random; the ratio stays close.

Wait times#

The wait time decides the pause between tasks. There are three built-in kinds:

Three kinds of wait time constant waits a fixed time after each task, between waits a random time, and constant_pacing starts a task at a fixed rhythm. constant(1) task, then wait 1 s between(0.5, 1.5) task, then wait 0.5 to 1.5 s constant_pacing(1) one task STARTS every 1 s blue = the task running, grey = waiting
constant waits a fixed time after each task, between waits a random time, and constant_pacing starts a task at a fixed rhythm.
import time

from locust import User, between, constant, constant_pacing, events, task

runs = {"ConstantUser": 0, "RandomUser": 0, "PacedUser": 0}


class Base(User):
    abstract = True

    @task
    def work(self):
        time.sleep(0.3)               # the task itself takes 0.3 seconds
        runs[type(self).__name__] += 1


class ConstantUser(Base):
    wait_time = constant(1)           # 1 s of waiting AFTER each task


class RandomUser(Base):
    wait_time = between(0.5, 1.5)     # random 0.5 to 1.5 s after each task


class PacedUser(Base):
    wait_time = constant_pacing(1)    # one task START every 1 s, whatever the task time


@events.test_stop.add_listener
def show(environment, **kwargs):
    name = environment.user_classes[0].__name__
    print(f"{name}: ran its task {runs[name]} times in 6 seconds")

Each class was run alone for 6 seconds (locust -f 02_waits.py ConstantUser -u 1 ...). The task takes 0.3 s.

ConstantUser: ran its task 5 times in 6 seconds
RandomUser: ran its task 5 times in 6 seconds
PacedUser: ran its task 6 times in 6 seconds
  • constant(1): waits 1 s after each task, so one round is 1.3 s long, giving about 5 runs in 6 seconds.

  • between(0.5, 1.5): a random pause, 1.3 s on average, so roughly the same count (a little random).

  • constant_pacing(1): starts a task every 1 s including its running time, so you get steady throughput (6 runs) however long the task takes. Use it when you want an exact rate.

No wait_time at all means no pause, which hammers the server as fast as it will go. Real users always pause.

Common mistakes#

  • Forgetting wait_time. The users never pause and flood the server much harder than real people would.

  • Thinking @task(3) means “three times per second”. It is a relative weight, not a rate.

  • Reading constant(1) as “one task per second”. It waits one second after each task; use constant_pacing for a fixed rate.

  • Heavy work inside tasks. Do not parse big files or call slow libraries in a task; it makes your load generator the bottleneck.

  • Expecting identical numbers each run. Choices are random; compare ratios and trends.

Hands-on exercises#

Exercise 1. Add a third task search with weight 6 to Visitor. About what share of the tasks should it be?

Solution

The weights are 3, 1 and 6, so search should be 6 / 10 = 60% of tasks, browse 30% and buy 10%. Run it and compare the counts; they should be close to those shares.

Exercise 2. You want exactly 2 requests per second per user. Which wait time?

Solution

constant_pacing(0.5): a new task starts every 0.5 s regardless of how long the task takes (as long as the task itself takes under 0.5 s).

Exercise 3. Change PacedUser’s task to sleep 1.5 s instead of 0.3. How many runs do you expect in 6 seconds? (Try it.)

Solution

Pacing cannot start a task before the previous one finishes, so each round takes 1.5 s. Completions land at 1.5, 3.0, 4.5 and 6.0 seconds, so you expect 3 or 4 runs. When we ran it we measured 3, because the fourth would finish exactly as the test stopped. Locust cannot keep the rhythm when the task is slower than the pace.

Practice#

Take the Session 2 quiz. The original pages @task decorator, task attribute and Understanding wait times are in the reference section.