Last modified: Oct 14, 2026

Write Locust Tasks in Python

A Locust test is only as good as its tasks. Tasks describe what each simulated user actually does. Get them right and your load test tells the truth.

Get them wrong and you measure the wrong thing. A test full of identical requests says nothing about how real users behave.

This guide shows you how to write Locust tasks in Python. You will learn about weights, sequences, tags, validation, and the small details that make a test realistic.

What a Task Actually Is

A task is a method inside a user class. Locust picks a task at random, runs it, waits, then picks another.

Every task makes one or more requests. Locust records the status code, response time, and payload size for each one.

That record is what you see in the live statistics. No task, no data.

The simplest possible task is a single HTTP call wrapped in a method.

Setting Up Before You Write Tasks

You need Locust installed before anything else. If it is not ready yet, follow this guide on how to install Locust in Python first.

Create a file named locustfile.py. Locust looks for that name by default when you start a test run.

Keep the file in a dedicated folder. Mixing test files with application code causes confusion later.

Your First Task

Start with one user class and one task. Do not overthink it.

 # locustfile.py from locust import HttpUser, task, between class ApiUser(HttpUser): wait_time = between(1, 3) @task def get_status(self): """Request the health endpoint.""" self.client.get("/api/health") 

The @task decorator marks the method as a task. Without it, Locust ignores the method completely.

The between() function sets a random pause between one and three seconds. That pause happens after each task, not before.

The self.client.get() method sends the request and records the result. Locust handles timing and statistics for you.

Adding Weights to Tasks

Real users do not perform every action equally. They view lists far more often than they submit forms.

Task weights let you model that. Pass a number to the decorator and Locust uses it as a relative frequency.

 from locust import HttpUser, task, between class ShopUser(HttpUser): wait_time = between(1, 2) @task(5) def browse_products(self): self.client.get("/api/products") @task(2) def view_cart(self): self.client.get("/api/cart") @task(1) def checkout(self): self.client.post("/api/checkout", json={"items": [1, 2]}) 

In this setup, browsing runs five times for every one checkout. That matches how a real store behaves.

Weights are relative, not percentages. The numbers 5, 2, and 1 produce the same mix as 50, 20, and 10.

Never set all weights to the same value if your traffic is not uniform. That is one of the most common testing mistakes.

Running Tasks in Order with SequentialTaskSet

Sometimes tasks must follow a fixed sequence. A login must happen before a profile update.

The SequentialTaskSet class runs tasks in the order they are declared. Each user loops back to the start when the sequence finishes.

 from locust import HttpUser, task, between, SequentialTaskSet class PurchaseFlow(SequentialTaskSet): @task def open_product(self): self.client.get("/api/products/42") @task def add_to_cart(self): self.client.post("/api/cart", json={"product_id": 42}) @task def place_order(self): self.client.post("/api/orders", json={"cart_id": "abc"}) class BuyingUser(HttpUser): wait_time = between(1, 2) tasks = [PurchaseFlow] 

The PurchaseFlow class runs its three tasks in order. Every simulated user completes the whole journey before repeating.

Attach the class to a user with the tasks attribute. You can mix sequential sets with plain tasks in the same user class.

Using Tags to Organize Tasks

Large test files accumulate many tasks. Tags let you run a subset without editing the file.

 from locust import HttpUser, task, between, tag class TaggedUser(HttpUser): wait_time = between(1, 2) @tag("read") @task def list_items(self): self.client.get("/api/items") @tag("write") @task def create_item(self): self.client.post("/api/items", json={"name": "widget"}) 

Run only the read tasks from the command line with the tags flag.

 locust -f locustfile.py --tags read --host http://localhost:8000 

This is useful when you want to isolate a slow endpoint. Exclude tags with the exclude-tags flag when you need the opposite.

Tasks with Dynamic Data

Identical requests hide real problems. If every user posts the same payload, the API caches everything and looks fast.

Generate unique data per user with a counter or a random value.

 import random from locust import HttpUser, task, between class DataUser(HttpUser): wait_time = between(1, 3) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.user_id = random.randint(1, 100000) @task def create_record(self): payload = { "name": f"user-{self.user_id}", "score": random.randint(1, 100), } self.client.post("/api/records", json=payload) 

The __init__ method runs once per simulated user. Storing the ID there avoids generating a new one on every request.

Read shared test data from a file at import time. Load it once, then sample from the list inside tasks.

Keep test data out of source control if it contains real customer information.

Validating Task Responses

HTTP 200 does not guarantee correctness. An API can return a success status with an error message inside the body.

Wrap the request so you can inspect the response before Locust records it.

 from locust import HttpUser, task, between class CheckUser(HttpUser): wait_time = between(1, 2) @task def fetch_report(self): with self.client.get("/api/report/1", catch_response=True) as response: if "total" not in response.text: response.failure("Missing total field in response") elif response.elapsed.total_seconds() > 2: response.failure("Response too slow") else: response.success() 

The catch_response() argument hands control to your code. Locust waits for a verdict instead of deciding on its own.

The response.failure() method logs the request as failed with your message. The response.success() method marks it as passed.

This pattern catches slow responses too. Latency assertions are often more valuable than status assertions.

Grouping Requests with Custom Names

Dynamic URLs create chaos in your statistics. Every unique path becomes its own row in the report.

Use the name argument to group them under one label.

 from locust import HttpUser, task, between class GroupedUser(HttpUser): wait_time = between(1, 2) @task def fetch_item(self): item_id = 42 self.client.get(f"/api/items/{item_id}", name="/api/items/[id]") 

All requests to that pattern appear as a single row. The aggregated numbers become readable again.

You can also issue a raw request when you need full control over the method and URL.

 from locust import HttpUser, task, between class RawUser(HttpUser): wait_time = between(1, 2) @task def custom_call(self): self.client.request("PATCH", "/api/items/42", json={"qty": 3}) 

The self.client.request() method accepts any HTTP verb and passes the same options as the shortcut methods.

Setup and Teardown Hooks

Some work should happen once per user, not once per task. Login is the classic example.

 from locust import HttpUser, task, between class HookUser(HttpUser): wait_time = between(1, 2) def on_start(self): response = self.client.post("/api/login", json={ "user": "demo", "pass": "demo" }) self.token = response.json()["token"] def on_stop(self): self.client.post("/api/logout") 

The on_start() method runs when a user begins. The on_stop() method runs when the user finishes.

Store the token on the instance. Every later task can read it without logging in again.

Do not hammer the login endpoint in every task. That distorts your results and may trigger rate limits.

Running Your Task File

Start Locust with the file you wrote and point it at your API.

 locust -f locustfile.py --host http://localhost:8000 --users 20 --spawn-rate 2 

The console shows a summary when the test starts.

 [2026-10-14 10:00:00,000] INFO/locust.main: Starting Locust 2.32.3 [2026-10-14 10:00:00,001] INFO/locust.runners: Ramping to 20 users at a rate of 2.00 per second Type Name # reqs # fails Avg Min Max GET /api/products 820 0 41 11 180 GET /api/cart 310 0 38 10 150 POST /api/checkout 160 0 95 30 410 Aggregated 1290 0 48 10 410 

The distribution reflects your weights. If products show roughly five times the checkout count, the weighting works as intended.

For a broader walkthrough of test design and result reading, see this guide on how to load test APIs with Locust. It covers authentication, distributed runs, and result export in more depth.

When your suite matures, you can also schedule Python tasks with Airflow to run the test nightly and catch regressions before release.

Common Task Mistakes

Equal weights everywhere. Uniform tasks produce unrealistic traffic and misleading bottlenecks.

No wait time. Zero pauses create machine-gun traffic that no real user would generate.

Login inside every task. Authentication traffic dominates the numbers and hides real performance.

Identical payloads. Caches and database indexes behave differently under repeated identical data.

Unnamed dynamic URLs. Your report fills with hundreds of single-request rows.

No response validation. Errors slip through because the status code looked fine.

Tasks that depend on each other. Random task selection breaks shared state. Use a sequential set instead.

Best Practices for Writing Tasks

Model real journeys, not isolated endpoints. A user who adds an item should also check out.

Keep each task small. One clear action per method makes failures easy to trace.

Name methods clearly. Your future self reads the statistics report and needs to recognize each row.

Use tags from the start. Adding them later across hundreds of tasks is tedious.

Version your test files alongside your application. An API change should trigger a test update in the same pull request.

Keep imports tidy as the file grows. A quick pass with a tool that can sort Python imports automatically with isort keeps the test file readable.

Conclusion

Locust tasks are just Python methods with a decorator. That simplicity is what makes the tool so flexible.

Start with one task and one request. Add weights to reflect real traffic. Group steps into sequences when order matters.

Validate responses, name dynamic URLs, and use hooks for login. Those four habits separate a useful test from a noisy one.

Write tasks that mirror what your users actually do. Then your load test stops being a number and starts being a prediction.