Residential Proxies Web Scraping API Turn Sites Into AI Data Automate 3000+ Apps Learn Python Automation Pay As You Go Proxies
Residential Proxies Web Scraping API
Pay As You Go Proxies 10 Free Proxies Antidetect Browser No Code Browser Bots Web Data For AI Agents Hire Scraper Builders

Locust Load Testing: Write Load Tests in Python Step by Step

Updated August 2026
Locust is an open source load testing tool where user behavior is defined as ordinary Python code: an HttpUser class with task methods describes what each simulated user does, and Locust runs thousands of them concurrently while charting results in a live web UI. Because tests are plain Python, anything Python can do, generate fake data, read CSVs, handle complex auth, is available inside your load test. This tutorial builds a realistic locustfile and runs it locally, headless in CI, and distributed across workers.

Step 1: Install Locust

Locust needs Python 3.9 or newer. Install it into a virtual environment like any Python dependency:

python -m venv venv
source venv/bin/activate
pip install locust

Verify with locust --version. Under the hood Locust uses gevent for concurrency, cooperative green threads rather than OS threads, which is how a single process sustains thousands of simulated users. That detail matters once, later, when choosing between user counts and worker counts, and the rest of the time Python just looks like Python.

Step 2: Write Your First Locustfile

Locust looks for a file named locustfile.py. A realistic starter:

from locust import HttpUser, task, between
import random

class ShopUser(HttpUser):
    wait_time = between(2, 5)

    @task(3)
    def browse(self):
        self.client.get("/")
        self.client.get("/category/laptops")

    @task(2)
    def view_product(self):
        pid = random.randint(1, 500)
        self.client.get(f"/product/{pid}", name="/product/[id]")

    @task(1)
    def add_to_cart(self):
        pid = random.randint(1, 500)
        self.client.post("/cart/add", json={"product_id": pid, "qty": 1})

Each simulated user picks tasks weighted by the numbers in @task: browsing three times as often as carting, mirroring real traffic mix. wait_time = between(2, 5) inserts 2 to 5 seconds of think time between tasks, without which your user counts would wildly overstate the simulated load. The name="/product/[id]" argument groups five hundred distinct URLs into one statistics row, so reports show one "/product/[id]" line instead of five hundred one-request lines.

The client is a requests-compatible session per user, cookies persist across a user's tasks automatically, so a login in an on_start method carries through the whole session. on_start is where per-user setup lives: authenticate, pick a persona, load a data slice.

Step 3: Run It and Watch the Web UI

Start Locust and point it at your target:

locust --host https://staging.example.com

Open http://localhost:8089, enter a user count and spawn rate, say 100 users spawning at 5 per second for a gentle ramp, and start. The UI charts requests per second, response time percentiles, and user count live, with a statistics table showing per-endpoint request counts, failure counts, median, p95, and p99. The Failures tab lists every distinct error with counts, and the Charts tab is where you watch for the telltale bend: response times climbing while throughput flattens, the saturation signature explained in our performance testing introduction.

The live UI is Locust's standout feature for exploratory testing: you can raise the user count mid-run, watch the system respond in real time, and find the knee of the curve interactively in one session, a workflow that batch-oriented tools need several scripted runs to replicate.

Step 4: Validate Responses, Not Just Timing

By default Locust counts HTTP error statuses as failures, but a 200 carrying an error page counts as success unless you check. The catch_response context manager fixes that:

with self.client.get(f"/product/{pid}", name="/product/[id]", catch_response=True) as res:
    if "Add to cart" not in res.text:
        res.failure("product page missing buy button")
    elif res.elapsed.total_seconds() > 2:
        res.failure("over 2s budget")

Marking slow-but-successful responses as failures encodes your latency budget into the test itself. This is also where Python earns its keep: pull expected values from a database fixture, validate JSON schemas with a library, or compute checksums, whatever "correct" means for your system, you can assert it inline.

For data variety beyond random integers, load real test data at module level, a CSV of product IDs weighted by actual popularity, a pool of test accounts, and draw from it in tasks. Caches then see realistic hit patterns instead of either one endlessly repeated key or a uniform spray that no real traffic resembles.

Step 5: Headless Runs for CI

The web UI is for humans; pipelines run headless:

locust --headless -u 50 -r 5 -t 3m --host https://staging.example.com --csv results --exit-code-on-error 1

That spawns 50 users at 5 per second, runs three minutes, writes results_stats.csv and friends, and exits nonzero if any requests failed. For latency-based gating, check the p95 column of the stats CSV in a small script after the run, or define the budget inside the locustfile with an event hook that quits with a bad exit code when percentiles cross budget. Either way the pattern matches the CI approach in our CI/CD test automation guide: short run, modest load, strict budget, red build on regression.

Step 6: Distributed Mode for Serious Load

One Locust process is bound to one CPU core by design, and gevent concurrency within it typically sustains a few hundred to a few thousand users depending on task weight. Past that, run distributed: one master coordinating, N workers generating.

locust --master --host https://staging.example.com
locust --worker --master-host 192.168.1.10

Start one worker per core, on one machine or across many; workers connect to the master, which distributes users among them and aggregates results into the same web UI and CSVs. The locustfile must be present on every worker. For heavy HTTP throughput per worker, swap HttpUser for FastHttpUser, which trades a slightly different client API for several times more requests per second per core.

If managing worker fleets becomes its own job, cloud services will run Locust for you, BlazeMeter supports Locust scripts on managed infrastructure, and Kubernetes operators exist for teams already living there. The comparison with k6, Gatling, and JMeter in our tools roundup covers when a higher-throughput engine beats scaling Locust horizontally: broadly, Locust wins on expressiveness and Python fit, k6 and Gatling win on raw load per machine.

Locustfile Patterns Worth Stealing

A few structures recur in mature locustfiles. Multiple user classes model distinct populations, a BrowserUser, a SearchUser, an AdminUser, each with its own task mix, and Locust spawns them proportionally to a weight attribute, giving you a traffic model instead of a single average user. SequentialTaskSet scripts strict funnels like signup flows where order matters. Event hooks (test_start, request, test_stop) integrate custom logging, seed data setup, and teardown. And because a locustfile is a module, shared helpers, auth flows, payload builders, ID pools, live in ordinary Python files imported by several test suites, versioned in the same repository as the application they test.

Teams building Python skills to write better tests, or better backends, can lean on structured project-based learning like Zero to Mastery while adopting these patterns; none of them require advanced Python, just the discipline to treat load tests as real code with reviews and version history.

Key Takeaway

Locust turns load testing into Python programming: HttpUser classes with weighted tasks model realistic traffic, wait_time keeps the simulation honest, catch_response encodes correctness and latency budgets, and the live web UI makes exploratory capacity testing interactive. Run headless with CSV output and exit codes for CI, go distributed with master and workers when one core is not enough, and reach for FastHttpUser before adding machines. For Python teams it is the load testing tool that tests actually keep getting written in.