View all services
Talk to QA Advisor
Browse the Knowledge Hub211 resources
Tools Library · Review and guide

Locust review: load testing tutorial, pros and cons

Locust is an open-source load testing tool in which each simulated user is a Python class and its behaviour is ordinary Python code. It runs thousands of users per process on gevent greenlets, distributes load across processes and machines with a built-in master and worker mode, and shows results live in a web UI or headless in CI. It is free under the MIT licence.

QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.

last reviewed
13 October 2026
version covered
Locust 2.46
licence
Open source

Reviewed by Viral Patel

Co-Founder, QAbleLast reviewed 13 October 2026

At a glance

Locust at a glance

Official site
Vendor
Locust project
Category
Performance testing
Licence
Open source (MIT)
Platforms
Cross-platform
Languages
Python
Integrates with
Docker, Kubernetes, OpenTelemetry, Azure Load Testing, GitHub Actions
First released
2011
Version reviewed
Locust 2.46

Verdict

Our verdict on Locust

Strengths

  • Users are Python classes, so test logic, data handling and helpers are ordinary code that reviews and diffs cleanly
  • Master and worker mode is built in; --processes forks one worker per core on a single Linux or macOS machine
  • The live web UI shows throughput, percentiles and failures and lets you change user count mid-run
  • Any protocol with a gevent-friendly Python client can be load tested by firing request events yourself
  • LoadTestShape gives code-level control of users and spawn rate over time for spikes, steps and waves
  • Free under MIT, with no paid edition or account required for any feature

Weaknesses

  • One process uses one CPU core, so a single process caps out early and must be scaled by hand
  • Closed workload model by default: request rate falls when the system slows unless you pace users
  • Blocking client libraries that are not gevent-friendly stall every user in the process
  • No built-in pass or fail thresholds: CI gating means writing an event listener
  • --processes relies on fork, so it does not work on Windows

Our take

Locust fits teams whose engineers already write Python and want load tests that read like the rest of their code: loops, conditionals, data handling and helper modules with no tool-specific constructs. It is particularly comfortable for API and web back ends, and for unusual protocols where writing a small client is easier than finding a plugin. QAble has evaluated it rather than maintained a long-running suite on it, so treat this as a bounded take. Two things to plan for. Each Locust process uses one CPU core, so serious load means several processes from the start, not as an afterthought. And it is a closed-model tool by default, so request rate depends on response times unless you pace users deliberately. Also note that the Locust Cloud hosted service has been shut down; the docs now point to Azure Load Testing for managed runs.

We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.

How it works

How Locust works

Greenlets, not threads

Locust creates one instance of your User class for every simulated user and runs each in its own greenlet, a lightweight coroutine managed by gevent. gevent patches the standard library so that network calls and time.sleep yield to other greenlets instead of blocking, which is why a task can be written as plain sequential code and still run thousands of users in one process. The cost of that design shows up when code does not yield: a CPU-heavy loop, or a database or message client that does its own blocking I/O outside gevent’s patches, stops every user in the process until it returns. The docs say this plainly for custom clients: use a gevent-friendly library, or you are effectively limited to one user per worker.

One core per process, so distribution is the norm

Python cannot fully use more than one core per process, so a single Locust process saturates one core and then starts under-reporting what the system can do. Locust logs a CPU warning when that happens. The answer is built in: one master process coordinates and aggregates statistics, and worker processes run the users and report back over ZeroMQ, on port 5557 by default. The master runs no users itself. On one machine, --processes 4 forks a master and four workers, and --processes -1 starts one worker per logical core. Across machines, start a master and point workers at it with --master-host; workers can even fetch the locustfile from the master with -f -.

Tasks, weights and wait time

Each user repeatedly picks a task at random, weighted by the number given to the @task decorator, runs it to completion, then sleeps for its wait_time before picking again. Requests inside a task run in order, so a task is a natural unit for one user journey step. User classes can be weighted against each other, or pinned to an exact count with fixed_count. Wait time is applied after a task, not between requests, and it is what turns users into load: between and constant model think time, constant_throughput and constant_pacing cap how often a task runs, and poisson, added in 2.46.6, samples gaps from an exponential distribution so arrivals bunch and spread like real traffic.

Statistics come from request events

Every request made through self.client fires a request event carrying its name, response time, length and any exception. Locust aggregates these by request name into the table you see in the web UI and console, with percentiles, failure counts and a history used for the charts. In distributed mode workers send their statistics to the master, which merges them. Because the event is the unit, anything that fires one is measured the same way: the HTTP clients do it for you, and a custom client for gRPC, MQTT or a database only needs to fire the event with a name and timing. It also explains why unique URLs must be grouped with name, or the table grows a row per URL.

Strengths

What it does well

Test logic in plain Python

Branching, loops, random data, parsing a response to decide the next call, importing a team’s existing API client: all of it is ordinary Python, written in your usual editor and reviewed like any other code. There is no XML plan or DSL to learn, and larger suites split naturally into packages such as common/auth.py that every locustfile imports.

A live web UI you can steer

The web UI at port 8089 starts a run, shows requests per second, response time percentiles, failures and exceptions as they happen, and lets you change the number of users while the test is running. Data downloads as CSV from the same page. It is useful for exploratory capacity work, where you want to raise load step by step and watch where latency bends.

Distributed runs without extra products

Master and worker mode, --processes for one machine, an official locustio/locust Docker image with a Docker Compose example, and a Kubernetes operator installed by Helm all come from the project. Nothing about distribution is reserved for a paid tier, which keeps the cost of scaling to the machines you run.

Custom load shapes in code

A LoadTestShape subclass with a tick method returns the desired user count and spawn rate about once a second, or None to stop. That gives full control of spikes, steps, waves and soak profiles, and each stage can restrict which user classes spawn. Shapes can live in their own files and be combined with a locustfile on the command line.

Protocols beyond HTTP

HttpUser wraps python-requests and FastHttpUser wraps geventhttpclient. Recent releases add contributed users such as SocketIOUser, MqttUser, MilvusUser and DNSUser, and for anything else you write a client that fires the request event. That makes Locust a practical way to load test an internal protocol for which no other tool has support.

Reports and telemetry for automation

Headless runs can write CSV files with --csv, a self-contained HTML report with --html and final statistics as JSON on stdout with --json. Event hooks such as init, test_start, test_stop and quitting let you seed data, push metrics or set the exit code, and OpenTelemetry support, added in 2.42.4, exports to an existing observability stack.

Pricing

Locust pricing and licensing

Locust is free and open source under the MIT licence, with no paid edition, no licence key and nothing held back. The real costs are the load generator machines, the CI minutes and the engineering time to write and maintain the Python. The project’s own hosted service, Locust Cloud, has been shut down, and the docs now point to Azure Load Testing, a Microsoft service that runs Locust scripts and is priced by Microsoft. Microsoft also sponsors Locust’s maintenance.

  • Licence: MIT, stated in the repository and in pyproject.toml. Commercial use, modification and redistribution are all permitted.
  • No tiers: distributed mode, the web UI, HTML reports, custom shapes and the Kubernetes operator are all in the open-source project.
  • Infrastructure: you pay for the machines that generate load. Because each process uses one core, budget one worker per core and enough cores for the target rate, plus headroom so the generators never become the bottleneck.
  • Hosted option: Azure Load Testing runs Locust tests as a managed service with reporting and CI integration, on Microsoft’s pricing. Locust Cloud is no longer available; the 2.43.0 changelog removed references to it as it shut down.
  • Hidden cost: the CI pass and fail logic, data handling and reporting conventions are code your team writes and maintains, since Locust ships primitives rather than opinions.

Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.

Getting started

Locust tutorial: getting started

Setup, step by step

  1. Install Python 3.11 or later; Locust 2.46.1 dropped Python 3.10.
  2. Install Locust with pip in a virtual environment, or run it without installing through uvx.
  3. Save a locustfile.py in the current folder with one HttpUser class and a task.
  4. Run locust, open http://localhost:8089, and enter the user count, spawn rate and host.
  5. Watch the statistics and failures tabs, then stop the test and download the CSV data.
  6. Repeat headless with --headless, -u, -r and --run-time to see the CI form of the same run.

Install

python -m pip install locust
locust -V
# or, without a permanent install
uvx locust -V

A first example

# locustfile.py
from locust import HttpUser, task, between

class ShopUser(HttpUser):
    # Think time after each task, in seconds
    wait_time = between(1, 3)

    def on_start(self):
        # Runs once per simulated user; session cookies are kept
        self.client.post("/api/login", json={"email": "[email protected]", "password": "secret"})

    @task(3)  # picked three times as often as checkout
    def browse(self):
        self.client.get("/api/products")
        # Group every product id under one statistics row
        self.client.get("/api/products/42", name="/api/products/[id]")

    @task
    def checkout(self):
        with self.client.post("/api/cart", json={"sku": "A1"}, catch_response=True) as resp:
            if "cartId" not in resp.text:
                resp.failure("No cartId in response")

# Run headless against a test environment you own:
# locust -f locustfile.py --headless -u 50 -r 5 --run-time 2m --host https://staging.example.com

What trips people up first

Reading user count as load. A new user sets 100 users and expects 100 requests per second, then sees far fewer, or sees the rate fall as the system slows. Locust is a closed model: each user waits for a response, then for its wait_time, before starting the next task, so throughput is users divided by the time each iteration takes. Decide which number you are testing against. For a target rate, use constant_throughput or constant_pacing and enough users to sustain it. The second surprise is the CPU warning on a laptop: one process uses one core, so add --processes before trusting high-load numbers.

In practice

Using Locust in practice

Organising a Locust project

A locustfile is an ordinary Python module, and the working directory is on sys.path, so treat a load test suite like any small Python package. Keep shared code such as authentication, test data loading and custom clients in a common package, one locustfile per system or journey under a locustfiles folder, and dependencies pinned in requirements.txt. Put run settings in locust.conf or a [tool.locust] section of pyproject.toml rather than long command lines, so the default run is reproducible and reviewable. Settings are applied in a fixed order: pyproject.toml, then locust.conf, then a --config file, then LOCUST_ environment variables, then command-line arguments, so CI can override the host or duration without editing the file. Use @tag on tasks and --tags to run a smoke subset from the same code.

# locust.conf: picked up automatically from the working directory
locustfile = locustfiles/shop.py
headless = true
host = https://staging.example.com
users = 200
spawn-rate = 20
run-time = 10m
csv = results/shop
html = results/shop.html

Pass and fail rules for CI

By default a headless run exits with code 1 if any request failed, and --exit-code-on-error changes that code. Nothing fails a run for being slow, which is the usual reason a performance gate exists. The documented pattern is a listener on the quitting event that inspects the aggregated statistics and sets environment.process_exit_code. Keep it in a separate module, such as common/thresholds.py, and import it from every locustfile so every run applies the same rules. Choose thresholds from a baseline run against the same environment, and prefer percentiles to averages: an average hides the slow tail that users notice. Check them per request name too, using environment.stats.get with the name and method, when one endpoint matters more than the total.

# common/thresholds.py: import this from each locustfile
import logging
from locust import events

@events.quitting.add_listener
def apply_thresholds(environment, **kwargs):
    total = environment.stats.total
    p95 = total.get_response_time_percentile(0.95)
    if total.fail_ratio > 0.01:
        logging.error("Failed: more than 1% of requests failed")
        environment.process_exit_code = 1
    elif p95 > 800:
        logging.error("Failed: 95th percentile %.0f ms is over 800 ms", p95)
        environment.process_exit_code = 1
    else:
        environment.process_exit_code = 0

A GitHub Actions load test job

Run load tests on a schedule or on demand against a dedicated test environment, not on every pull request against shared staging, and never against a system you do not own. The job below installs dependencies, runs four worker processes with a time limit, and keeps the CSV and HTML output whether the thresholds pass or not. PYTHONUNBUFFERED makes the console output appear as it happens, and a job timeout stops a hung run from holding a runner. A hosted runner is small, so treat it as a regression check at modest load: for capacity testing, run workers on machines sized for the job, or on Kubernetes with the operator, and keep the same locustfile.

# .github/workflows/load-test.yml
name: load-test
on:
  workflow_dispatch:
  schedule:
    - cron: '0 2 * * 1-5'
jobs:
  locust:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    env:
      PYTHONUNBUFFERED: 1
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: '3.13'
          cache: pip
      - run: pip install -r requirements.txt
      - run: mkdir -p results
      # Settings come from locust.conf; the host is overridden per environment
      - run: locust --processes 4 --host ${{ vars.LOAD_TEST_URL }}
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: locust-results
          path: results/

Scaling out to real load

Watch the generators as closely as the system under test. If Locust logs that CPU usage is above 90 percent, its numbers are no longer trustworthy. In order: switch hot paths to FastHttpUser, which the docs describe as using significantly less CPU than HttpUser; add processes up to the core count; then add machines. For multiple machines, start the master with --master and --expect-workers so a headless run waits for every worker, and start workers with --worker --master-host. The official Docker image and its Compose example make a master plus scaled workers a one-line command, and the Kubernetes operator manages master and worker jobs as a custom resource. Keep generators close to the target network-wise, and spread them if flood protection or a load balancer pins traffic from few addresses to few instances.

Shaping load over time

Users and spawn rate describe a ramp to a plateau. For anything else, such as a step test that holds each level long enough to read, a spike, or a soak with a warm-up, write a LoadTestShape. Locust uses it automatically when it finds one in the loaded files, calls tick about once a second, and stops the test when tick returns None. Keeping shapes in separate files means one locustfile serves several profiles: locust -f locustfile.py,step_shape.py. If user start-up is slow, for example a login per user, get_current_user_count lets a stage wait until the previous level is actually reached before moving on.

# step_shape.py: run with  locust -f locustfile.py,step_shape.py
from locust import LoadTestShape

class StepShape(LoadTestShape):
    # (end time in seconds, users, spawn rate)
    stages = [
        (120, 50, 10),
        (300, 100, 10),
        (480, 200, 20),
        (600, 50, 20),  # ramp down to check recovery
    ]

    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  # stops the test

Pitfalls

Common pitfalls

A single process reported as the system’s limit

Why it happens: One Locust process uses one CPU core. When it saturates, response times include time spent waiting for the load generator, and the throughput ceiling is Locust’s rather than the system’s.

The fix: Watch for the CPU warning in the log, run one worker per core with --processes or master and worker mode, and use FastHttpUser where the request volume is high.

Blocking libraries freezing every user

Why it happens: gevent only switches greenlets when code yields. A database driver or SDK that blocks outside gevent’s patched standard library holds the whole process, so one slow call stalls every user in it.

The fix: Use gevent-friendly clients, or pure-Python ones that use the patched socket module, and measure them with a small test before scaling. Keep CPU-heavy work such as large payload generation out of tasks.

Following tutorials written for Locust 0.x

Why it happens: Older articles use HttpLocust, a task_set attribute, -c for clients, --hatch-rate and min_wait with max_wait. Locust 1.0 renamed the classes and flags, --hatch-rate was removed in 2.32.6, and min_wait and max_wait are deprecated.

The fix: Use HttpUser or FastHttpUser, tasks on the User class or a tasks list, -u and -r, and a wait_time function such as between or constant_throughput. Check the changelog for anything else a tutorial relies on.

Statistics tables with thousands of rows

Why it happens: Statistics are grouped by request name, which defaults to the URL. Ids and query strings in the URL create a row per value, which hides the real picture and costs memory on long runs.

The fix: Pass name to group dynamic URLs, for example name="/orders/[id]", or wrap a block in client.rename_request when a library makes the calls for you.

A green CI run despite a slow system

Why it happens: The default exit code only reflects failed requests. A run where every request succeeded slowly exits 0, so a pipeline gate built on the exit code passes regressions.

The fix: Add a quitting listener that sets environment.process_exit_code from percentiles and failure ratio, and import it from every locustfile so the rules cannot be forgotten.

--processes failing on Windows

Why it happens: The --processes option relies on fork, which Windows does not provide, so a setup that works on a Mac or in Linux CI fails on a Windows workstation.

The fix: Use WSL or the official Docker image on Windows, or start a master and several workers by hand with --master and --worker.

Compared

Locust alternatives compared

ToolTest languageLicenceConcurrency modelDistributed runsBuilt-in reporting
LocustPythonMITGreenlets, one core per processMaster and workers, built in, freeLive web UI, CSV, HTML report
k6JavaScript, TypeScriptAGPL-3.0Go engine, many VUs per processKubernetes operator or Grafana Cloud k6End-of-test summary, outputs to backends
Apache JMeterGUI-built XML plans, Groovy scriptingApache 2.0One thread per virtual userController and remote servers over RMIHTML dashboard report
GatlingJava, Kotlin, Scala, JavaScript, TypeScriptApache 2.0, paid Enterprise editionAsynchronous, non-blocking engineGatling Enterprise onlyHTML report after each run

FAQ

Frequently asked questions

What are the best Locust alternatives?

The alternatives most often weighed against Locust are k6, Apache JMeter and Gatling. The comparison on this page sets them side by side on test language, licence, concurrency model, distributed runs and built-in reporting, and the verdict above says where Locust is the wrong choice and which of them to pick instead.

Is Locust free?

Yes. Locust is open source under the MIT licence, with no paid edition and no features held back; distributed mode, the web UI, reports and the Kubernetes operator are all free. Your costs are the machines that generate load and the time to write and maintain the Python. For a managed service, Microsoft’s Azure Load Testing runs Locust scripts on its own pricing.

Should I choose Locust or k6?

Choose by language and by how much load each machine must produce. Locust suits Python teams and tests that need real program logic or a custom protocol client. k6 suits JavaScript or TypeScript teams, generates more load per core from its Go engine, and has built-in thresholds and open-model executors. Both are code-first and fit CI. If your team writes Python every day, Locust is the more natural fit.

Locust or JMeter?

JMeter builds test plans in a GUI and saves them as XML, and its strength is protocol breadth through samplers and plugins. Locust tests are Python code, which diffs and reviews cleanly and handles complex logic more naturally. JMeter has a richer out-of-the-box HTML dashboard; Locust has a better live UI for steering a run. Teams that prefer writing code to configuring components usually find Locust easier to maintain.

How many users can Locust simulate?

There is no fixed number; it depends on your tasks, payloads and hardware. The docs say one process can run thousands or tens of thousands of users if their total request rate is not too high, and that the ceiling is CPU. Measure it: run your test on one process, raise users until Locust logs a CPU warning, then scale out with one worker per core and more machines.

Can Locust test protocols other than HTTP?

Yes. Any system with a Python client can be tested by wrapping the client so it fires Locust’s request event with a name and response time. Recent releases also include contributed users for Socket.IO, MQTT, Milvus and DNS. The client must be gevent-friendly, otherwise a blocking call stalls every user in the process.

What happened to Locust Cloud?

The Locust Cloud hosted service was shut down. Locust 2.36.0 had added a --cloud option, and 2.43.0 removed references to it as the service closed. The Locust documentation now lists Azure Load Testing, a Microsoft-managed service, as the hosted way to run Locust tests at scale. Self-hosted distribution with master and worker processes, Docker or Kubernetes is unaffected.

Does Locust run a real browser?

No. HttpUser and FastHttpUser send HTTP requests; they keep cookies but do not parse HTML, fetch page resources or execute JavaScript. Locust measures server-side behaviour under load. For front-end timings, run a small browser-based check alongside the load test rather than trying to drive thousands of browsers.

Which Python versions does Locust support?

Locust 2.46 requires Python 3.11 or later, according to its package metadata, after 2.46.1 removed Python 3.10 support. Python 3.15 support was added in 2.46.0. If you are stuck on an older Python, you must pin an older Locust, which also means older documentation, so upgrading Python is usually the better path.

Alternatives

Locust alternatives worth weighing

Resources

Where to go next

Ready

Picking the wrong tool costs more than picking late.

QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.

Talk to QA Advisor