Skip to main content

Locust Load Testing: A Python Tutorial With Real Output

Locust load testing step by step: install, write a locustfile, run headless, read the stats and shape the load. Tested on Locust 2.47.0 with 1,026 requests.

Locust Load Testing: A Python Tutorial With Real Output

Locust load testing means describing user behaviour as Python code, in a file called a locustfile, and letting Locust run many of those users at once against your site or API. You install it with pip install locust, write a class that inherits from HttpUser with @task methods, and run locust with a user count, a spawn rate and a duration.

This tutorial builds one locustfile from scratch, runs it against a small local shop API, and shows the exact output Locust 2.47.0 printed, including the failures it caught and what each column means.

Key Takeaways

  • Locust 2.47.0 (October 2026) needs Python 3.11 or later. On Python 3.10, pip install locust quietly installs the older 2.46.0.
  • A locustfile is a Python class: on_start for login, @task(weight) for each action, wait_time for think time.
  • Use name= to group URLs with IDs into one stats row, and catch_response=True to fail responses that return 200 with the wrong content.
  • Read percentiles per endpoint. In our run the Aggregated median was 1 ms while the product endpoint’s median was 56 ms.
  • One Locust process uses one CPU core; use --processes or workers to scale past it.

What is Locust?

Locust is an open-source load testing tool, released under the MIT licence, in which every test is ordinary Python. Each simulated user is an instance of a class you write, and Locust runs many of them concurrently on gevent, reporting response times, request rates and failures in a web UI or the terminal. The current release is 2.47.0, published on 21 October 2026.

Because tests are code, a scenario can do anything Python can: generate data, read files, branch on a response, call a library. The trade-off is that Locust only knows what your code tells it. HttpUser “is not a real browser”, so it does not load images, scripts or stylesheets from an HTML page, though it does keep cookies between requests like a browser session.

If you are still choosing a tool, our k6 vs JMeter vs Gatling vs Locust comparison covers where Locust fits against the others.

How do you install Locust?

Install Locust from PyPI into a virtual environment, then check the version it reports.

python3 -m venv venv
source venv/bin/activate
pip install locust
locust -V

Check that last line. Locust 2.46.1 dropped Python 3.10, and when we ran pip install locust on Python 3.10.11 it installed 2.46.0 without any warning. With Python 3.12 the same command gave locust 2.47.0 ... (Python 3.12.13). If locust -V shows an older version than the one on PyPI, your Python is the reason.

How do you write a locustfile?

A locustfile needs at least one class that inherits from User; for HTTP testing that is HttpUser, which gives every simulated user a self.client that works like a requests session and records each request in Locust’s statistics.

Our test target is a small shop API with four endpoints: a home page, a product endpoint that takes 20 to 80 ms and fails with HTTP 500 about 2% of the time, a login that returns a token, and a cart that needs that token and now and then returns HTTP 200 without its items key. Here is the whole locustfile:

import random

from locust import HttpUser, task, between


class ShopUser(HttpUser):
    host = "http://127.0.0.1:8080"
    wait_time = between(1, 3)

    def on_start(self):
        # Runs once per simulated user, before its first task.
        resp = self.client.post("/login", json={"username": "demo", "password": "demo"})
        self.token = resp.json()["token"]

    @task(3)
    def browse(self):
        self.client.get("/")
        product_id = random.randint(1, 1000)
        # Without name=, every id would be its own row in the stats table.
        self.client.get(f"/products?id={product_id}", name="/products?id=[id]")

    @task(1)
    def view_cart(self):
        headers = {"Authorization": f"Bearer {self.token}"}
        with self.client.get("/cart", headers=headers, catch_response=True) as response:
            if response.status_code != 200:
                response.failure(f"cart returned {response.status_code}")
            elif "items" not in response.json():
                response.failure("cart response has no 'items' key")

What each part does:

  • host is the base URL added to every request. You can override it with --host on the command line or in the web UI.
  • on_start runs once for each user when it starts. Logging in here means each user gets its own token, which later tasks reuse.
  • @task(3) and @task(1) are weights: a user picks browse three times as often as view_cart. Only methods with @task are picked.
  • wait_time = between(1, 3) pauses each user for a random 1 to 3 seconds after every task. Without a wait_time, the next task starts as soon as the previous one ends.
  • name="/products?id=[id]" groups every product URL under one row. Locust groups statistics by URL, so without it each ID becomes a separate entry.
  • catch_response=True lets you decide whether a response passed. Locust counts any status below 400 as a success, so a 200 with a broken body needs this check to be counted as a failure.

The wait_time setting decides how much load each user generates. Locust ships five helpers:

HelperWhat it doesUse it for
constant(n)Waits exactly n seconds after each taskSimple, predictable pacing
between(a, b)Waits a random time between a and b secondsHuman think time
constant_throughput(n)Adapts the wait so the task runs at most n times per secondA target request rate per user
constant_pacing(n)Adapts the wait so the task runs at most once every n secondsFixed iteration intervals
poisson(rate)Random waits from an exponential distribution, so tasks arrive as a Poisson processRealistic arrival patterns

These apply to tasks, not requests: our browse task makes two requests, so it produces two requests per wait.

How do you run a Locust load test?

Run locust in the folder that holds locustfile.py and open http://localhost:8089 to start a test from the web UI, where you set the number of users and the spawn rate and watch the charts live. For repeatable runs and CI, skip the UI with --headless:

locust -f locustfile.py --headless -u 20 -r 5 -t 60s \
    --csv results/shop --html results/report.html
FlagMeaning
--headlessNo web UI; start immediately
-u 20Peak number of concurrent users
-r 5Users started per second until the peak is reached
-t 60sStop after 60 seconds, counted from the start of the test, ramp-up included
-HBase URL, if the locustfile does not set host
--csv results/shopWrite statistics to CSV files with that prefix
--html results/report.htmlSave the HTML report

With 20 users and a spawn rate of 5, Locust logged All users spawned: {"ShopUser": 20} (20 total users) three seconds after it started. The --csv option wrote four files: shop_stats.csv, shop_stats_history.csv, shop_failures.csv and shop_exceptions.csv.

How do you read Locust results?

When the run ends, Locust prints a statistics table, a percentile table and an error report. This is the statistics table from our 60-second run, with the padding trimmed:

Type  Name                # reqs  # fails    | Avg  Min  Max  Med | req/s  failures/s
GET   /                      439  0(0.00%)   |   0    0    8    1 |  7.36        0.00
GET   /cart                  129  2(1.55%)   |   0    0    3    1 |  2.16        0.03
POST  /login                  20  0(0.00%)   |   2    1    7    3 |  0.34        0.00
GET   /products?id=[id]      438  7(1.60%)   |  57   23   89   56 |  7.34        0.12
      Aggregated            1026  9(0.88%)   |  24    0   89    1 | 17.20        0.15

And two rows of the percentile table, in milliseconds (the 99.9% and 99.99% columns left out; Locust labels these percentiles approximated, which is why the 100% column reads 90 where the stats table’s Max reads 89):

Name                 50%  66%  75%  80%  90%  95%  98%  99%  100%
/products?id=[id]     56   66   72   77   83   85   89   89    90
Aggregated             1   39   51   57   74   82   85   88    90

Four things to take from this output.

Read each endpoint, not the Aggregated row. The Aggregated median of 1 ms is real but meaningless: more than half of the requests (568 of 1,026) went to the fast home page and cart, which hid the product endpoint’s 56 ms median and 85 ms 95th percentile. Our guide to why percentiles beat averages explains why the tail is the number to watch.

The error report separates failure types. It listed 7 GET /products?id=[id]: HTTPError('500 Server Error ...'), which Locust detected from the status code, and 2 GET /cart: cart response has no 'items' key, which only our catch_response check could catch, because those responses were HTTP 200.

name= works, with one cost. All 438 product requests sit in one row with meaningful percentiles. In a 15-second control run without name=, the stats file had 100 separate /products?id=N rows. The cost: error messages show the grouped name, so the failing ID is not recorded.

The exit code is a CI gate. Locust exited with code 1, which it does by default whenever any request fails. A pipeline step that runs Locust headless fails the build on errors without extra scripting; --exit-code-on-error changes the code.

The throughput figure is set by the user count and wait_time: 20 users waiting 1 to 3 seconds between tasks produced about 17 requests per second. To push harder, add users or shorten the waits.

How do you shape the load over time?

For anything other than a straight ramp to a fixed user count, add a LoadTestShape class. It can sit in the locustfile itself, or, as here, in a separate file that imports your user class. Locust calls its tick() method about once per second; it returns the user count and spawn rate to use, or None to stop the test.

from locust import LoadTestShape

from locustfile import ShopUser  # reuse the same user class


class StepShape(LoadTestShape):
    # (end time in seconds, users, spawn rate)
    stages = [
        (10, 10, 5),
        (20, 30, 10),
        (30, 0, 30),
    ]

    def tick(self):
        run_time = self.get_run_time()
        for end, users, rate in self.stages:
            if run_time < end:
                return (users, rate)
        return None  # stop the test

Save it as locustfile_shape.py and run it without -u, -r or -t, since the shape controls all three:

locust -f locustfile_shape.py --headless

Our run logged exactly the steps we asked for:

Shape test updating to 10 users at 5.00 spawn rate
Shape test updating to 30 users at 10.00 spawn rate
Shape test updating to 0 users at 30.00 spawn rate
Shape test completed, shutting down

The same pattern builds a spike test (a short jump to a high count and back) or a long soak at a steady level.

Should you use HttpUser or FastHttpUser?

FastHttpUser is, in the docs’ words, a drop-in replacement for HttpUser “in most cases”, built on geventhttpclient instead of python-requests. The Locust docs say it “uses significantly less CPU time, sometimes increasing the maximum number of requests per second on a given hardware by as much as 5x-6x”, and also that “it does not make individual requests faster”. Start with HttpUser, and switch when a load generator’s CPU becomes the limit.

Our run matched that. Swapping the base class and running the same 20 users for 60 seconds gave 17.4 requests per second and a product median of 57 ms, against 17.2 and 56 ms with HttpUser. At about 17 requests per second, far below the thousands per process the docs describe, the load generator’s CPU was not the limit, so there was nothing to save. FastHttpUser pays off when a single load generator is CPU-bound at high request rates. One visible difference: it reports 500s as LocustBadStatusCode(code=500) instead of HTTPError.

How do you run Locust on several CPU cores or machines?

Python cannot fully use more than one core per process, so one Locust process is limited to one CPU core. The docs’ best case for a single process with small requests is around 4,000 requests per second with HttpUser and 16,000 with FastHttpUser.

To use every core on one machine, start Locust with --processes 4 (or -1 to detect the core count). It launches a master and that many workers. This relies on fork(), so it does not work on Windows.

Across machines, start one instance with --master and the others with --worker --master-host <master address>. The master runs the web UI and tells workers when to start and stop users; the workers run the users and send statistics back. -f - on a worker fetches the locustfile from the master, for single-file tests.

Hosted options for running Locust from many regions

Distributed Locust is infrastructure you operate: worker machines or Kubernetes pods in each region you want traffic from, kept on the same version as the master. For a hosted option the Locust docs now point to Azure Load Testing, and the docs’ changelog for version 2.43.0 records that references to locust.cloud were removed “now that it is shutting down”.

LoadFocus does not run locustfiles. It runs JMeter and k6 scripts, and no-code tests built in the browser, from 25 cloud regions with no load generators to manage. If your scenarios are simple enough to rebuild, cloud load testing gives you multi-region runs and a history of results; if writing tests in Python is the reason you chose Locust, keeping Locust is a sound choice. The Locust alternative page sets out that trade-off.

Frequently Asked Questions

Is Locust good for load testing?

Yes, for teams that write Python. Locust tests are plain Python classes, so complex user flows, custom data and response checks are easy to express, and it runs headless in CI with a failing exit code. It tests at the protocol level rather than in a real browser, and one process uses one CPU core.

Is Locust free?

Yes. Locust is open source under the MIT licence and installs from PyPI with pip install locust. You pay only for the machines that run it. Hosted services that run Locust tests for you, such as Azure Load Testing, are charged separately.

Which Python version does Locust need?

Locust 2.47.0 requires Python 3.11 or later and lists versions 3.11 to 3.15. Version 2.46.0 was the last to support Python 3.10, and pip on Python 3.10 installs it instead of the latest release without a warning.

How many users can Locust simulate?

It depends on the scenario and the hardware. The Locust docs put the best case for one process with small requests at around 4,000 requests per second with HttpUser and 16,000 with FastHttpUser. A user that waits a few seconds between tasks makes less than one request per second, so the user count can be many times the request rate. Because each process uses one CPU core, you scale with --processes on one machine or with master and worker instances across machines.

Does Locust run a real browser?

No. Locust’s HttpUser class sends HTTP requests and keeps cookies, but it does not parse HTML, download page resources or run JavaScript. To include page assets, request them explicitly in your tasks. To measure what users see in a browser, use a browser-based tool alongside it.

What is the difference between Locust and JMeter?

Locust tests are Python code, with HTTP built in and other protocols added through your own client code, while JMeter tests are built in a GUI, saved as .jmx files, and cover more protocols out of the box, including JDBC, JMS and FTP. Locust suits developers who want tests in code; JMeter suits testers who prefer a GUI.

Sources

  • Locust 2.47.0 release, Python requirement and licence: pypi.org/pypi/locust/json and github.com/locustio/locust (release 2.47.0, README, LICENSE), October 2026.
  • Writing a locustfile (HttpUser, tasks and weights, wait_time helpers, on_start, catch_response, name grouping): docs.locust.io, “Writing a locustfile”, October 2026.
  • Command line options (--headless, -u, -r, -t, --csv, --html) and custom load shapes: docs.locust.io, “Configuration”, October 2026.
  • Exit code on failures and run time counting: docs.locust.io, “Running without the web UI”, October 2026.
  • FastHttpUser and requests per second per process: docs.locust.io, “Increase performance”, October 2026.
  • Distributed runs, --processes and the one-core-per-process limit: docs.locust.io, “Distributed load generation”, October 2026.
  • Hosted load testing (Azure Load Testing) and the locust.cloud changelog entry: docs.locust.io, “Hosted load testing” and “Changelog”, October 2026.
  • Test runs: Locust 2.47.0 on Python 3.12.13, headless, 20 users for 60 seconds against a local Python test API, plus a FastHttpUser run, a no-name= control run and a LoadTestShape run, 2026-10-25.

Bogdan

Founder at LoadFocus

Bogdan builds and runs the tools this blog is about. He writes from what the products actually do in production, including the parts that break.

Weekly tips right in your inbox.

Don’t miss out on the latest testing tips and tricks, tools and news related to software testing.

Already +20,000 members in the list.

The Load Testing Platform with AI That Explains Your Results

Load test websites and APIs from 25 cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.

Cloud load testing, website speed and API monitoring in one place.

Hi there! Can I share a cool thing I'm working on with you?

A software testing newsletter that sparks joy.

Our goal is to create helpful content for software developers and testers.

If you're not into it, you can unsubscribe instantly.