Task weights in Locust are the primary mechanism for controlling the relative frequency with which different tasks are executed by your simulated users.
Let’s see this in action. Imagine we have two tasks: task_a which simulates a quick GET request, and task_b which simulates a slower POST request that we want to run less often.
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 5)
tasks = {
"task_a": 3, # task_a will run 3 times more often than task_b
"task_b": 1
}
@task
def task_a(self):
self.client.get("/a")
@task
def task_b(self):
self.client.post("/b")
In this setup, for every single execution of task_b, task_a will be chosen to execute three times. This ratio is crucial for accurately modeling real-world user behavior where certain actions are far more common than others.
The problem Locust solves is simulating realistic user load. If all tasks had an equal chance of running, you’d quickly overestimate the load on less frequent but potentially more resource-intensive operations, or underestimate the overall throughput of frequently executed actions. Task weights allow you to precisely tune your simulation to match observed production traffic patterns, giving you a more accurate picture of your system’s performance under stress.
Internally, when a user is ready to execute its next task, Locust picks one from the tasks dictionary. It does this by creating a weighted choice. If your tasks are {"task_a": 3, "task_b": 1}, Locust effectively creates a list of choices like [task_a, task_a, task_a, task_b] and randomly picks one. The higher the number associated with a task, the more times it appears in this conceptual list, and thus the higher its probability of being selected.
The exact levers you control are the integer values assigned to each task in the tasks dictionary. These are relative weights. {"task_a": 5, "task_b": 1} will behave identically to {"task_a": 10, "task_b": 2} in terms of execution probability. The absolute values don’t matter, only their proportion to each other.
What most people miss is that while the tasks attribute defines the pool of tasks a user can execute, the @task decorator without an argument implies a weight of 1. If you define a task with @task and it’s not present in the tasks dictionary, it won’t be executed. If it is present in the tasks dictionary, its weight is taken from there. If you use @task(weight=N) on a method, that overrides any weight defined in the tasks dictionary for that specific method.
Understanding how these weights translate into actual execution probabilities is key to designing effective load tests.
The next concept you’ll likely grapple with is how to dynamically adjust these weights or tasks based on observed system behavior during a test.