QA How-To
Locust load testing tutorial (2026)
Locust load testing tutorial for 2026: HttpUser tasks, headless CI, LoadTestShape ramps, distributed workers, auth flows, and interview Q&A for performance QA.
19 min read | 2,258 words
TL;DR
This Locust load testing tutorial shows how to write Python HttpUser classes, weight tasks, run headless CI load tests, shape ramps, and distribute workers. Focus on named requests, honest failure marking, and pass/fail gates tied to error ratio and latency SLOs.
Key Takeaways
- Model virtual users as HttpUser classes with weighted @task methods.
- Use wait_time for think time; do not confuse users with RPS.
- Group requests with name= to keep statistics readable.
- Run headless in CI with HTML/CSV artifacts and exit codes on failure.
- LoadTestShape encodes ramps and spikes as code.
- Scale with master/worker processes when one generator is not enough.
- Mark business failures with catch_response, not only HTTP status.
Locust load testing tutorial material should get you from install to a maintainable, CI-ready suite without cargo-cult scripts. Locust is a Python-based load tool where you write user behavior as code, run a local or distributed swarm of workers, and watch live charts in a web UI or headless mode. In 2026, teams pick Locust when they want expressive Python (factories, shared libs, pytest-adjacent skills) rather than JavaScript-centric k6 or GUI-first JMeter.
This Locust load testing tutorial covers installation, HttpUser classes, wait times, tasks, shape classes, headless CI runs, distributed workers, events hooks, and comparison points against k6 and JMeter. For modeling theory see designing a load model. For bottleneck triage after red results see finding a performance bottleneck.
TL;DR
| Piece | Role |
|---|---|
| HttpUser | Virtual user class with host and tasks |
| @task | Weighted user actions |
| wait_time | Think time between tasks |
| locustfile.py | Entry module Locust loads |
| headless | CI-friendly non-UI run |
| workers | Distributed generation |
Write tasks that mirror real journeys, run headless with explicit users and spawn rate, fail the build on error ratio or custom criteria, and scale out with workers when one machine is not enough.
1. When This Locust Load Testing Tutorial Applies
Choose Locust when:
- The team already writes Python fluently
- Scenarios need complex data setup, cryptography, or custom protocols via Python libraries
- You want code review of load tests in normal PRs
- You need a live UI for exploratory performance sessions
- Distributed execution should stay relatively simple
Consider k6 when JS/TS is the house language, or when you want first-class thresholds as a primary feature (Locust can still gate CI; patterns differ). Consider JMeter when enterprise GUI plans and existing plugin ecosystems dominate. A short comparison lives later in this Locust load testing tutorial; also see JMeter vs k6 for load testing for adjacent tooling context.
2. Install and Verify
Use a virtual environment:
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install locust
locust -V
Pin versions in requirements.txt:
locust==2.32.3
Version pins matter for CI reproducibility. Adjust the pin to the current stable release your org approves; do not invent APIs from memory when upgrading. Always read the release notes for breaking changes around event hooks and shape classes.
3. Your First locustfile
Create locustfile.py:
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 3)
@task(3)
def view_home(self):
self.client.get("/")
@task(1)
def view_contacts(self):
with self.client.get("/contacts.php", catch_response=True) as response:
if response.status_code != 200:
response.failure(f"unexpected status {response.status_code}")
else:
response.success()
Run against a public demo host (replace with your staging URL):
locust -f locustfile.py --host https://test.k6.io
Open the printed UI URL (default http://localhost:8089), enter user count and spawn rate, start swarming. This interactive loop is ideal for exploratory sessions. For automation, use headless mode shown later.
4. HttpUser, Tasks, and Weighting
HttpUser provides self.client, a session-aware HTTP client based on requests-style usage. Tasks are methods marked with @task or listed in a tasks dict/list. Weights control relative selection frequency: @task(3) runs about three times as often as @task(1) when both are eligible.
from locust import HttpUser, task, constant
class ApiUser(HttpUser):
wait_time = constant(0.5)
@task(10)
def list_items(self):
self.client.get("/news.php", name="list_news")
@task(1)
def heavy_page(self):
self.client.get("/contacts.php", name="contacts")
The name parameter groups URLs in statistics so query-string cardinality does not explode the report. High-cardinality names are a classic Locust reporting footgun.
5. Wait Times and Load Shape Reality
Wait times model think time:
from locust import between, constant, constant_pacing, constant_throughput
wait_time = between(1, 5) # uniform random delay
wait_time = constant(1) # fixed delay
# constant_pacing / constant_throughput help pace iterations more tightly
Closed-model style virtual users with think time do not equal a fixed RPS. If you need strict arrival rates, consider shaping with LoadTestShape, external pacing, or compare with k6 arrival-rate executors. Document which model your stakeholders think they bought.
6. on_start, on_stop, and Auth Flows
from locust import HttpUser, task, between
class AuthenticatedUser(HttpUser):
wait_time = between(1, 2)
def on_start(self):
# Example only: adapt to your auth API
response = self.client.post(
"/login",
json={"user": "qa", "password": "secret"},
name="login",
)
if response.status_code != 200:
self.environment.runner.quit()
return
token = response.json().get("token")
self.client.headers.update({"Authorization": f"Bearer {token}"})
def on_stop(self):
self.client.post("/logout", name="logout")
@task
def profile(self):
self.client.get("/profile", name="profile")
Never hardcode production passwords in git. Inject via environment variables:
import os
password = os.environ["LOADTEST_PASSWORD"]
Token refresh, cookie jars, and CSRF headers fit naturally in Python. That expressiveness is a major reason teams complete a Locust load testing tutorial and stay on the tool.
7. catch_response for Business Failures
HTTP 200 is not always success.
from locust import HttpUser, task, between
class SearchUser(HttpUser):
wait_time = between(0.5, 1.5)
@task
def search(self):
with self.client.get("/?q=test", name="search", catch_response=True) as resp:
if resp.status_code != 200:
resp.failure("bad status")
elif "error" in resp.text.lower():
resp.failure("error embedded in body")
else:
resp.success()
Marking failures correctly keeps error ratios honest. Silent soft failures are worse than noisy red charts.
8. Headless Mode for CI
locust -f locustfile.py \
--host https://staging.example.com \
--headless \
--users 50 \
--spawn-rate 5 \
--run-time 5m \
--html report.html \
--csv results
Useful flags:
--headless: no UI--users: total virtual users--spawn-rate: users started per second--run-time: duration (for example10m,1h)--html: HTML report path--csv: CSV prefix for stats--exit-code-on-error 1: non-zero exit when failures occur (confirm flag support for your version)
Example GitHub Actions job sketch:
jobs:
locust-smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -r requirements.txt
- run: >
locust -f locustfile.py
--host ${{ secrets.STAGING_URL }}
--headless --users 10 --spawn-rate 2 --run-time 2m
--html locust-report.html
- uses: actions/upload-artifact@v4
if: always()
with:
name: locust-report
path: locust-report.html
Align CI packaging ideas with add CI to a test framework.
9. LoadTestShape for Ramps and Stages
from locust import HttpUser, task, LoadTestShape, constant
class SiteUser(HttpUser):
wait_time = constant(1)
@task
def index(self):
self.client.get("/")
class StagesShape(LoadTestShape):
"""
Simple stage planner: (duration_seconds, user_count, spawn_rate)
"""
stages = [
{"duration": 60, "users": 10, "spawn_rate": 5},
{"duration": 180, "users": 50, "spawn_rate": 5},
{"duration": 240, "users": 0, "spawn_rate": 5},
]
def tick(self):
run_time = self.get_run_time()
for stage in self.stages:
if run_time < stage["duration"]:
return (stage["users"], stage["spawn_rate"])
return None
Shapes let a Locust load testing tutorial graduate from flat load to peaks and cool-downs without babysitting the UI. Keep stages in version control next to tasks.
10. Distributed Locust (Master and Workers)
One machine often cannot generate enough traffic. Locust supports distributed runs:
# master
locust -f locustfile.py --master --expect-workers 2 --headless --users 200 --spawn-rate 20 --run-time 10m --host https://staging.example.com
# worker nodes
locust -f locustfile.py --worker --master-host <master-ip>
Guidelines:
- Same locustfile version on all nodes
- Workers need network access to the target and to the master
- Watch worker CPU; saturated generators produce fake bottlenecks
- Prefer multiple modest workers over one overheated laptop
Containerize workers when platform allows, building on Docker basics for testers.
11. Events, Custom Metrics, and Listeners
from locust import events
import time
@events.request.add_listener
def on_request(request_type, name, response_time, response_length, exception, **kwargs):
if exception:
# hook for custom logging / metrics backend
pass
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
print("load test starting")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
print("load test stopping")
You can push metrics to your observability stack with custom listeners. Keep hooks lightweight; heavy work on every request skews the generator.
12. Data Parameterization
import csv
import os
from locust import HttpUser, task, between
def load_queries(path):
with open(path, newline="", encoding="utf-8") as f:
return [row["query"] for row in csv.DictReader(f)]
QUERIES = load_queries(os.environ.get("QUERIES_CSV", "queries.csv"))
class SearchUser(HttpUser):
wait_time = between(0.2, 1.0)
@task
def search(self):
q = QUERIES[self.environment.runner.user_count % len(QUERIES)]
self.client.get(f"/?q={q}", name="search")
Better approaches use per-user iterators or queues to avoid synchronized stampedes on the same row. Unique data matters for caches and DB locks. For API-centric plans, also read API performance testing tutorial.
13. Organizing Larger Projects
perf/
locustfile.py # thin entry, imports users
users/
browse.py
checkout.py
data/
queries.csv
shapes/
black_friday.py
requirements.txt
README.md
# locustfile.py
from users.browse import BrowseUser
from users.checkout import CheckoutUser
# Locust discovers User subclasses imported into the locustfile module tree
Shared Python packages let you reuse auth helpers across suites. Apply the same code review standards you use for production services: typing where helpful, no secrets, clear names.
14. Comparison Table: Locust vs k6 vs JMeter
| Factor | Locust | k6 | JMeter |
|---|---|---|---|
| Language | Python | JavaScript | GUI/XML + elements |
| Best developer UX for Python teams | Excellent | Fair | Fair |
| Built-in browser/UI | Yes (web UI) | Cloud/local summaries | GUI + listeners |
| Thresholds as first-class | Custom/CI logic | Native thresholds | Assertions + backends |
| Distributed mode | Master/workers | k6 cloud or OSS patterns | Engines |
| Protocol extensibility | Python ecosystem | JS modules / xk6 | Plugins |
There is no universal winner. This Locust load testing tutorial assumes Python-first teams validating HTTP-heavy systems.
15. Locust Load Testing Tutorial: Pass/Fail Criteria
Define gates before the run:
- Error ratio under agreed budget (for example under 1%)
- p95 for critical named requests under SLO
- No generator CPU saturation above agreed limit
- Functional checks on JSON fields for critical paths
Locust statistics expose medians, percentiles, and failure counts in UI/CSV/HTML. You may add a small post-processing script:
# check_stats.py (illustrative)
import csv
import sys
# Read Locust CSV stats and exit 1 if p95 exceeds limit for a row name
# Implement parsing against the CSV columns your Locust version writes.
Wire the script after headless runs in CI. Do not rely on humans staring at charts for release trains.
16. Common Scenario Patterns
Smoke: 5 users, 2 minutes, fail on any 5xx.
Baseline load: expected weekday peak users, 30-60 minutes.
Spike: jump users quickly with shape stages, watch recovery.
Soak: moderate users for hours, watch memory and error creep.
Mixed workload: multiple User classes with different weights imported into one locustfile.
Document which pattern each pipeline job runs. Naming jobs locust-smoke and locust-nightly prevents accidental full-scale runs on every commit.
17. Debugging Failures During a Run
- Start with one user to separate functional bugs from scale bugs
- Log response bodies sparingly with PII redaction
- Verify DNS and TLS to staging
- Check that
name=grouping is not masking a single bad URL - Confirm workers are connected in distributed mode
- Compare server-side RPS to Locust RPS for evidence of client limits
When the system melts, capture pod/node metrics if on Kubernetes (Kubernetes basics for testers) and application traces before restarting everything and losing evidence.
18. Security and Ethics of Load Generation
Only test systems you own or have written permission to stress. Throttle shared staging. Avoid credential stuffing patterns against real identity providers. Rotate load test accounts. Mark synthetic traffic in headers when ops requests it (X-Load-Test: 1) so SOC tools can filter.
Performance engineering without ethics is just uncoordinated downtime.
19. End-to-End Example Combining Ideas
import os
from locust import HttpUser, task, between, events
HOST = os.environ.get("TARGET_HOST", "https://test.k6.io")
class Shopper(HttpUser):
wait_time = between(1, 2)
def on_start(self):
self.client.headers.update({"X-Load-Test": "1"})
@task(5)
def browse(self):
self.client.get("/", name="home")
@task(1)
def contact(self):
with self.client.get("/contacts.php", name="contacts", catch_response=True) as r:
if r.status_code != 200:
r.failure("contacts failed")
else:
r.success()
@events.quitting.add_listener
def _(environment, **kwargs):
stats = environment.stats.total
if stats.fail_ratio > 0.01:
environment.process_exit_code = 1
# Optionally inspect entries for named request percentiles via environment.stats
export TARGET_HOST=https://test.k6.io
locust -f locustfile.py --host $TARGET_HOST --headless --users 30 --spawn-rate 5 --run-time 3m --html report.html
This pattern shows browse-heavy weighting, failure marking, synthetic header, and exit code control on quit. Adjust percentile enforcement to the stats APIs of your installed Locust version when you tighten SLOs.
20. Sequential Tasks With TaskSets
Some journeys must run in order: search, open detail, add to cart, start checkout. TaskSets and sequential task sets help structure that flow.
from locust import HttpUser, TaskSet, task, between, sequential_task
class CheckoutFlow(TaskSet):
@task
def step_open_form(self):
self.client.get("/contacts.php", name="checkout_form")
@task
def step_done(self):
self.interrupt() # return control to parent user scheduling
class Shopper(HttpUser):
wait_time = between(1, 2)
tasks = [CheckoutFlow]
@task(5)
def browse(self):
self.client.get("/", name="home")
Exact sequential helpers can vary slightly by Locust version; prefer the documented SequentialTaskSet patterns in your installed docs when order is strict. The important idea in this Locust load testing tutorial is intentional control flow rather than pure random task soup for transactional paths.
21. FastHttpUser When HTTP Performance Matters on the Client
Locust also offers FastHttpUser based on a different HTTP client stack for higher throughput from the same hardware. Use it when generator efficiency becomes the limit and your script only needs the supported client features.
from locust import FastHttpUser, task, constant
class CheapClient(FastHttpUser):
wait_time = constant(0.1)
@task
def index(self):
self.client.get("/")
Validate behavioral parity with HttpUser before switching CI gates. Faster clients that skip features you relied on (certain auth flows, upload patterns) can create false confidence.
22. Environment-Specific Configuration Module
# config.py
import os
class Settings:
host = os.environ.get("TARGET_HOST", "https://test.k6.io")
password = os.environ["LOADTEST_PASSWORD"] if os.environ.get("LOADTEST_PASSWORD") else None
smoke_users = int(os.environ.get("SMOKE_USERS", "5"))
full_users = int(os.environ.get("FULL_USERS", "100"))
Import settings from user classes and CI scripts. Central config prevents hardcoding staging hosts in ten files. Combine with README examples for local runs.
23. Reading HTML and CSV Reports Effectively
After headless runs, open the HTML report and inspect:
- Failures by request name
- Response time percentiles for critical names
- Number of users and RPS over time
- Whether failures cluster at spawn peaks
CSV files feed custom SLO scripts and long-term trend stores. Keep raw artifacts for major releases. A Locust load testing tutorial is incomplete if you only start tests and never institutionalize report reading in triage meetings.
24. Pairing Locust With Observability
Load without telemetry is incomplete. During runs, watch:
- Application RED metrics (rate, errors, duration)
- Saturation (CPU, memory, thread pools, DB connections)
- Dependency latency (cache, DB, third parties)
- Autoscaling events
Annotate dashboards with test start/stop times. Correlate Locust error spikes with deploy events. This is how performance QA partners with SREs rather than throwing CSV files over the wall.
25. Multiprocessing and Resource Hygiene
Each virtual user is not a full OS process in the same way as some tools, but your tasks still open sockets, allocate response bodies, and may spawn threads via libraries. Practices:
- Stream or discard large bodies when you only need status codes
- Close custom connections in
on_stop - Avoid per-request logging at high RPS
- Cap response capture in failure handlers
@task
def light_get(self):
with self.client.get("/", name="home", catch_response=True) as resp:
if resp.status_code != 200:
resp.failure("bad status")
# do not print(resp.text) here at scale
Generator hygiene is part of honest performance work. A Locust load testing tutorial that ignores client-side waste teaches people to blame servers for laptop limits.
26. Multi-User Classes in One Run
from locust import HttpUser, task, between
class Browser(HttpUser):
weight = 10
wait_time = between(1, 3)
@task
def home(self):
self.client.get("/", name="home")
class ApiClient(HttpUser):
weight = 2
wait_time = between(0.2, 0.5)
@task
def news(self):
self.client.get("/news.php", name="news")
Class-level weight balances how many users of each type Locust spawns. This is how you approximate mixed traffic without one god class. Document intended ratios next to the product load model.
27. Validating a New Script Before Peak Load
Checklist:
- Run with 1 user for 2 minutes; zero unexpected failures.
- Run with 10 users; confirm named stats look right.
- Watch generator CPU and network.
- Confirm server-side logs show synthetic header or test accounts.
- Only then scale to peak profiles.
Skipping step one is the most expensive habit in performance QA. Functional bugs under one user become chaotic charts under one hundred.
28. Example README Section for the Repo
# Performance (Locust)
## Smoke
locust -f locustfile.py --host $TARGET_HOST --headless --users 5 --spawn-rate 1 --run-time 2m --html smoke.html
## Full
locust -f locustfile.py --host $TARGET_HOST --headless --users 100 --spawn-rate 10 --run-time 20m --html full.html
## Secrets
export LOADTEST_PASSWORD=...
Operational docs turn a tutorial exercise into a team asset. Without them, only the author can run the suite.
Interview Questions and Answers
Q: What is Locust?
An open-source Python load testing tool where virtual users are classes, tasks define actions, and a master can coordinate distributed workers. It offers a web UI and headless CI execution.
Q: How do task weights work?
Locust picks tasks proportional to their weights. @task(3) is roughly three times as likely as @task(1) for that user class.
Q: How do you run Locust in CI?
Use headless mode with users, spawn rate, run time, and HTML/CSV outputs. Set a non-zero exit when error ratios or custom checks fail.
Q: Difference between wait_time and spawn rate?
Spawn rate controls how quickly users start. wait_time controls pauses between tasks for each running user.
Q: How do you model stages in Locust?
Implement a LoadTestShape subclass with tick() returning user count and spawn rate over time, or change values manually in the UI during exploratory runs.
Q: How does Locust scale out?
Run one master and multiple workers sharing the same locustfile; workers execute users and report metrics to the master.
Q: When would you pick Locust over k6?
When Python ecosystem fit, complex custom logic, or team skills dominate, and you accept implementing some SLO gates in Python/CI rather than native threshold DSL alone.
Common Mistakes
- Exploding stats with unique URL names per request.
- Treating virtual users as exact RPS without measuring.
- Hardcoding secrets in locustfiles.
- Load testing shared environments without coordination.
- Generator CPU saturation mistaken for SUT failure.
- Ignoring soft failures by only checking transport success.
- Running full-scale tests on every pull request.
- Not pinning Locust versions in CI.
- Synchronized test data causing unrealistic lock contention.
- Skipping one-user functional verification before scale.
- Forgetting distributed workers need matching code versions.
- Publishing internal staging URLs in public forks.
Conclusion
This Locust load testing tutorial walked from install through tasks, auth hooks, headless CI, shapes, and distributed workers. Locust shines when Python expressiveness and readable user classes matter. Pair honest wait times and named requests with clear pass/fail gates so charts become decisions.
Next, write a smoke locustfile against your staging host, add HTML artifacts to CI, then expand into shapes that match a documented load model. When results go red, follow a structured bottleneck process instead of only adding more users.
Interview Questions and Answers
Explain how Locust models load.
Locust runs virtual users defined as Python classes. Each user repeatedly selects weighted tasks and waits according to wait_time. Aggregate latency and failure stats measure system behavior under that concurrency.
How do you implement login once per user?
Use on_start to authenticate and store tokens or cookies on the user instance, then attach headers for subsequent tasks. Use on_stop for cleanup when needed.
What is LoadTestShape?
A class that controls user count and spawn rate over time through tick(), enabling staged ramps, spikes, and cool-downs without manual UI changes.
How do you keep Locust reports usable?
Pass stable name values to client calls so statistics group by logical endpoint instead of every unique URL permutation.
How would you scale a Locust test to higher RPS?
First ensure tasks and wait times match the model, then add workers in distributed mode and watch generator CPU so the client is not the bottleneck.
How do you mark a business error when HTTP status is 200?
Use catch_response=True, inspect the body or JSON fields, and call response.failure with a reason when the business contract is violated.
What belongs in a Locust CI smoke job?
A short headless run with few users, critical task coverage, HTML artifacts, and a strict enough failure gate to catch broken deploys without full peak load.
Frequently Asked Questions
What is covered in a Locust load testing tutorial for beginners?
Installation, a first locustfile with HttpUser and tasks, the web UI, headless runs, basic auth setup, and how to read failure ratios. Advanced sections add shapes and distributed workers.
How do I install Locust?
Create a virtualenv, pip install locust, and verify with locust -V. Pin the version in requirements.txt for CI.
How do I run Locust without the UI?
Use --headless with --users, --spawn-rate, --run-time, and optional --html/--csv outputs pointed at your --host.
Can Locust do distributed load generation?
Yes. Start a master and multiple workers with the same locustfile. Workers generate traffic and stream stats to the master.
How do task weights work in Locust?
Higher @task weights make those methods more likely to be selected relative to other tasks on the same user class.
How do I fail CI when Locust error rates are high?
Use exit-on-error options supported by your version and/or set environment.process_exit_code in quitting hooks after inspecting stats.
Is Locust better than k6?
It depends. Locust fits Python-centric teams and custom logic. k6 fits JS teams and native threshold-centric gates. Choose based on skills, protocol needs, and ecosystem.