Locust’s HttpUser isn’t just a way to define user behavior; it’s a sophisticated system for modeling how users interact with your HTTP service, and the most surprising thing is how much control you have over when and how requests are made, far beyond simple sequential execution.

Let’s see it in action. Imagine we have a simple web app and want to simulate users browsing it.

from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 5)  # Users wait 1-5 seconds between tasks

    @task
    def index(self):
        self.client.get("/")

    @task
    def about(self):
        self.client.get("/about")

    @task
    def products(self):
        self.client.get("/products")

When you run this with Locust, each WebsiteUser instance will pick one of these tasks randomly and execute it. After executing a task, it waits between 1 and 5 seconds before picking another task. This is the most basic form of task execution.

But HttpUser offers much more granular control. The @task decorator can take an argument: a weight. Higher weights mean a task is more likely to be chosen.

from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 5)

    @task(3)  # This task is 3 times more likely to be picked
    def index(self):
        self.client.get("/")

    @task(1)  # This task is less likely
    def about(self):
        self.client.get("/about")

    @task(1)
    def products(self):
        self.client.get("/products")

This allows us to model user journeys where certain actions are more frequent. The index page, for instance, is often visited more than the /about page.

Beyond simple weighted random selection, you can define explicit sequences of tasks using task_sets and nesting. This is crucial for modeling more complex user flows, like a user logging in, browsing products, and then logging out.

from locust import HttpUser, task, between, TaskSet

class UserBehavior(TaskSet):
    @task
    def browse_products(self):
        self.client.get("/products")

    @task
    def view_product_detail(self):
        # Assuming product IDs are integers from 1 to 10
        product_id = self.random.randint(1, 10)
        self.client.get(f"/products/{product_id}")

class WebsiteUser(HttpUser):
    wait_time = between(1, 5)
    tasks = [UserBehavior] # This HttpUser will exclusively run tasks defined in UserBehavior

Here, UserBehavior is a TaskSet. The HttpUser WebsiteUser is configured to use UserBehavior as its task source. Within UserBehavior, tasks are executed sequentially by default if they are not decorated with @task (which implies weighted random selection within the TaskSet). When using TaskSet, the HttpUser will execute tasks from the first TaskSet in its tasks list until it’s exhausted or a self.interrupt() is called, then it moves to the next TaskSet.

For more intricate flows, you can define multiple TaskSets and explicitly define transitions.

from locust import HttpUser, task, between, TaskSet, seq_task

class ProductFlow(TaskSet):
    @seq_task(1)
    def view_products_list(self):
        self.client.get("/products")

    @seq_task(2)
    def select_product(self):
        product_id = self.random.randint(1, 10)
        self.client.get(f"/products/{product_id}")

    @seq_task(3)
    def add_to_cart(self):
        # Simulate adding to cart, perhaps with a POST request
        self.client.post("/cart/add", json={"product_id": self.random.randint(1, 10), "quantity": 1})

class WebsiteUser(HttpUser):
    wait_time = between(2, 6)
    tasks = [ProductFlow]

Using seq_task guarantees that tasks within a TaskSet are executed in the specified order. The user will proceed through view_products_list, then select_product, then add_to_cart. If a task fails, the sequence might be interrupted depending on Locust’s error handling configuration.

The self.client object, an instance of HttpSession, is where all the request magic happens. It automatically handles cookies, maintains connection pooling, and reports statistics to Locust. You can also pass arguments to self.client methods that mirror the requests library, like headers, params, json, and auth.

Locust’s task execution isn’t strictly sequential or purely random; it’s a flexible system where you can define complex, multi-step user journeys by composing TaskSets and using weighted @task decorators. This allows for highly realistic simulation of user behavior by modeling not just what users do, but the order and probability of their actions.

One of the most powerful, yet often overlooked, aspects of HttpUser and TaskSet is the ability to dynamically generate tasks or interrupt task sequences. For example, you might want to simulate a user abandoning a cart after a certain amount of time or performing an action only under specific conditions. You can achieve this using self.interrupt() within a task to break out of the current TaskSet and potentially switch to another, or by using self.schedule_task() to queue up future tasks.

Understanding how to combine weighted tasks, ordered sequences, and dynamic interruptions is key to building sophisticated load tests that accurately reflect real-world user interactions.

The next step in mastering Locust’s user modeling is to explore how to parameterize your tasks, feeding them dynamic data derived from previous responses or external sources.

Want structured learning?

Take the full Locust course →