DS DevShelfHub Projects · AI tools
Cheatsheets / Locust
Cheatsheet · Dev tooling

Locust Cheatsheet: HttpUser, Tasks and Custom Shapes Reference

By DevShelfHub

User classes, tasks, weights, wait times, HttpUser, FastHttpUser, events, custom shapes, distributed mode — Python load testing.

105 items 8 min Users Tasks Shapes

Start hereQuick start · 6 you’ll reach for daily

Run UIlocust -f locustfile.py --host URL
Headlesslocust --headless -u 100 -r 10 -t 1m
Define userclass U(HttpUser): ...
Define task@task def x(self): ...
Group URLself.client.get(url, name="GET /x")
SLO gateevents.quitting → exit code

Target versions · paceVersions

Targets: locust ≥ 2.30 python ≥ 3.9 gevent (bundled)

Locust runs on gevent — every "user" is a greenlet, not an OS thread. That lets a single process drive thousands of virtual users on commodity hardware. The two HTTP clients are HttpUser (uses requests) and FastHttpUser (uses geventhttpclient, 3–10× faster). For more than a few thousand users, run distributed (--master / --worker) or single-host multi-core with --processes.

Install · runSetup

bash
# Install
pip install "locust>=2.30"
# Optional: gevent-friendly faster HTTP (already bundled, but you may pin)
pip install "geventhttpclient>=2"

# Project layout
#   locustfile.py            # auto-discovered when running `locust`
#   tests/load_*.py          # alternative — pass with -f
#   shapes.py                # custom LoadTestShape (optional)

# Hello world (locustfile.py)
cat > locustfile.py <<'EOF'
from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 3)

    @task
    def index(self):
        self.client.get("/")
EOF

# Run — web UI on http://localhost:8089
locust -f locustfile.py --host https://stg.example.com

# Headless (CI mode)
locust -f locustfile.py --host https://stg.example.com \
       --users 200 --spawn-rate 50 --run-time 2m \
       --headless --print-stats --html report.html --csv results

# Distributed (separate terminals / hosts)
locust -f locustfile.py --master                  # control plane
locust -f locustfile.py --worker --master-host=10.0.0.1
# Or scale workers with --processes (single-host multi-core)
locust -f locustfile.py --processes 4 --headless --users 1000 --spawn-rate 100 --run-time 5m

Where things liveCommon imports

from locust import HttpUser, FastHttpUser, UserBase classes. FastHttpUser for high throughput.
from locust import task, tag, constant, between, constant_pacingTask decorator + wait-time helpers.
from locust import TaskSet, SequentialTaskSetReusable task groups. Sequential runs in order.
from locust import eventsEvent hooks — start, stop, request, quitting…
from locust import LoadTestShapeProgrammatic ramp shapes.
from locust.runners import MasterRunner, WorkerRunner, LocalRunnerDetect distributed mode in event handlers.
from locust.contrib.fasthttp import FastHttpUser # legacy path, still worksOlder import path — new code uses top-level.

HttpUser · FastHttpUserUser classes

class WebUser(HttpUser): wait_time = between(1, 3)Wait between tasks. requests-based.
class WebUser(FastHttpUser): ...Faster Gevent HTTP — 3–10× throughput.
host = "https://example.com"Default base URL. Override with --host.
weight = 3Spawn ratio across user classes. 3:1 means 3× more of this user.
fixed_count = 2Pin exactly N of this user, ignoring weight.
def on_start(self): ...Per-VU setup — runs once when the VU spawns. Log in here.
def on_stop(self): ...Per-VU teardown.
abstract = TrueMark a base class — Locust skips spawning it.
tasks = [Browse, Buy] / tasks = {Browse: 3, Buy: 1}Declare tasks as a list/dict instead of decorators.

@task · TaskSetTasks & TaskSets

@task
def index(self): self.client.get("/")
Bare decorator. Weight 1.
@task(3)Weight 3 — runs 3× as often as a weight-1 task.
@tag("smoke", "search")Tag for CLI filtering: --tags smoke / --exclude-tags.
class Checkout(TaskSet): @task def add(self): ...Reusable group — tasks pick one randomly per iteration.
class Flow(SequentialTaskSet): ...Ordered Tasks run top-to-bottom in source order.
self.interrupt()Leave the TaskSet, return to the parent user’s task pool.
self.interrupt(reschedule=False)Skip the next task pick — bypass wait_time.
self.schedule_task(other_task)Queue another task explicitly.

between · pacingWait time

wait_time = between(1, 5)Random uniform seconds between tasks.
wait_time = constant(1)Always wait 1s.
wait_time = constant_pacing(1)RPS-style Iteration takes exactly N seconds, including task time.
wait_time = constant_throughput(5) # locust ≥ 2.10Cap each VU at N iterations/sec.
def wait_time(self): return random.lognormvariate(0, 0.5)Custom distribution — any callable returning seconds.

self.clientHTTP client

self.client.get(url, name="GET /items")GET. name groups dynamic URLs in stats.
self.client.post(url, json={...}) / data=, files=, headers=POST — same kwargs as requests.
self.client.put / .delete / .patch / .head / .optionsOther verbs.
self.client.request("METHOD", url, ...)Generic form.
self.client.headers["Authorization"] = "Bearer ..."Persistent header on this VU’s session.
self.client.cookies / self.client.cookies.set(...)Per-VU cookie jar.
with self.client.get(url, catch_response=True) as r: ...Preferred Manual pass/fail.
r.failure("reason") / r.success()Mark a response — only inside catch_response.
r.elapsed.total_seconds() / r.status_code / r.json()Response inspection.
self.rest("GET", "/items") # FastHttpUser convenienceAuto-parses JSON, raises on errors.
python
from locust import HttpUser, FastHttpUser, SequentialTaskSet, TaskSet, task, between, tag

class Browser(HttpUser):
    """Slow, realistic — full requests session, cookies, redirects."""
    wait_time = between(1, 3)
    weight = 3                                  # 3:1 vs Buyer

    @task(5)                                    # weighted within this user
    def browse(self):
        self.client.get("/items", name="GET /items")  # group ad-hoc URLs

    @task
    @tag("smoke", "search")
    def search(self):
        self.client.get("/search?q=widget", name="GET /search")

    def on_start(self):                         # login once per VU
        self.client.post("/login", json={"u": "x", "p": "y"})


class Buyer(FastHttpUser):
    """3-10x faster, no requests session — use when scaling matters."""
    wait_time = between(0.5, 2)
    weight = 1

    @task
    class Checkout(SequentialTaskSet):          # tasks run in order, top-to-bottom
        @task
        def add_to_cart(self):
            self.client.post("/cart", json={"sku": "abc"})

        @task
        def pay(self):
            with self.client.post("/checkout", catch_response=True, name="POST /checkout") as r:
                if r.status_code != 201:
                    r.failure(f"unexpected {r.status_code}")
                elif r.elapsed.total_seconds() > 1:
                    r.failure("too slow")

        @task
        def stop(self):
            self.interrupt()                    # return to outer pool

LoadTestShapeCustom shapes

Without a shape, Locust spawns to --users at --spawn-rate and holds. A shape overrides that with a programmatic schedule.

class S(LoadTestShape): def tick(self): return (users, rate)Override tick() — called every second.
return NoneFrom tick: stop the test.
self.get_run_time()Seconds since start — switch on elapsed time.
self.get_current_user_count()VUs currently running.
use_common_options = TrueHonour --run-time alongside the shape.
put shape in any .py file passed to locustAuto-discovered. Only one shape class per run.
python
# shapes.py — define load over time programmatically
from locust import LoadTestShape

class StagesShape(LoadTestShape):
    """Linear ramp-up, hold, spike, ramp-down."""
    stages = [
        # (run_time_seconds, target_users, spawn_rate)
        ( 30,  10, 5),    # warm-up
        ( 90,  50, 10),   # baseline
        (120, 200, 50),   # peak
        (150,  50, 20),   # cool-down
    ]

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


# Use it: locust -f locustfile.py shapes.py --headless
# (Locust picks up the LoadTestShape subclass automatically.)

# Other built-in shapes:
#   DoubleWaveShape, StepLoadShape (from locust-plugins)
#   or roll your own using __ENV / time.time() for dynamic patterns.

hooks for observabilityEvents

@events.init.add_listenerEarliest hook — before any user spawns. Add CLI args here.
@events.test_start.add_listenerTest run started.
@events.test_stop.add_listenerTest run stopped (UI stop or --run-time end).
@events.quitting.add_listenerSLO gate Last hook — set environment.process_exit_code to fail CI.
@events.request.add_listenerPer-request — export to Prometheus, count custom failures.
@events.user_error.add_listenerUnhandled exception in a task — logs, alerting.
@events.spawning_complete.add_listenerAll users spawned (steady state begins).
@events.report_to_master / @events.worker_reportDistributed-mode message passing.

flags you actually useCLI flags

-f locustfile.py / -f file1.py file2.pyFiles to load. Default locustfile.py in CWD.
-H / --host URLBase host. Overrides class-level host.
-u / --users NPeak number of VUs.
-r / --spawn-rate NVUs spawned per second.
-t / --run-time 5mStop after duration. Supports s/m/h.
--headlessNo web UI — the CI mode.
--web-host / --web-port 8089Bind the UI.
--csv results --csv-full-historyWrite timestamped CSV history.
--html report.htmlEnd-of-run HTML report (graphs + stats).
--master / --worker / --master-host / --master-portDistributed mode.
--processes NSingle-host scale Auto-spawn N worker processes on one machine.
--tags smoke / --exclude-tags slowFilter by @tag(...).
--stop-timeout 30Seconds to let users finish their current task before exit.
--config locust.confRead flags from a config file.

master · workersDistributed mode

locust -f f.py --masterCoordinator — serves the web UI, no traffic itself.
locust -f f.py --worker --master-host=10.0.0.1Connect a worker.
locust -f f.py --processes -1One worker per CPU core (single host).
--expect-workers NMaster waits until N workers connect before starting.
User count is divided across workersSpawn rate per worker = total / worker_count.
isinstance(env.runner, MasterRunner)Gate logic that should only run on the master.
@events.report_to_master / @events.worker_reportPush custom metrics across the boundary.
Locust scales horizontally — the master’s CPU caps total throughput. If the master pegs at 100%, add more workers or shed UI usage.

login → browse → checkoutEnd-to-end · full flow

FastHttpUser shopper, weighted browse vs. checkout, per-VU login in on_start, and a quitting event that turns p95 + error-rate into a CI exit code.

python
# locustfile.py — login, browse, checkout with events & SLO gating
import os, json, logging
from locust import FastHttpUser, task, between, events
from locust.runners import MasterRunner

BASE = os.getenv("API_URL", "https://stg.example.com")

with open("users.json") as f:                   # loaded once per worker process
    USERS = json.load(f)

class Shopper(FastHttpUser):
    host = BASE
    wait_time = between(0.5, 2)

    def on_start(self):
        u = USERS[self.environment.runner.user_count % len(USERS)]
        r = self.client.post("/api/login", json=u, name="POST /api/login")
        self.token = r.json()["token"]
        self.client.headers["Authorization"] = f"Bearer {self.token}"

    @task(4)
    def browse(self):
        self.client.get("/api/items", name="GET /api/items")

    @task(1)
    def checkout(self):
        with self.client.post("/api/orders", json={}, catch_response=True,
                              name="POST /api/orders") as r:
            if r.status_code != 201:
                r.failure(f"status={r.status_code}")

# Fail the run when SLOs miss
@events.quitting.add_listener
def _check_slo(environment, **kw):
    s = environment.stats.total
    if s.fail_ratio > 0.01:
        environment.process_exit_code = 1
        logging.error(f"fail ratio {s.fail_ratio:.2%} > 1%")
    if s.get_response_time_percentile(0.95) > 500:
        environment.process_exit_code = 1
        logging.error(f"p95 {s.get_response_time_percentile(0.95)}ms > 500ms")

@events.test_start.add_listener
def _on_start(environment, **kw):
    if isinstance(environment.runner, MasterRunner):
        logging.info(f"Starting against {BASE}")

Best practiceGood to know

Group dynamic URLs with name="...". Without it, /users/123 and /users/124 become separate rows in the stats table — one per real user.
Reach for FastHttpUser when throughput matters. HttpUser uses requests, which holds GIL-fighting locks — ~3–10× slower per VU. Switch when one node can’t generate the load.
Locust doesn’t fail CI by default. A run with 100% errors still exits 0 unless you set environment.process_exit_code in @events.quitting. Make this a habit.

Common trapsWatch out for

Don’t mix requests with HttpUser sessions. Calling requests.get(url) bypasses Locust’s instrumentation entirely — the request runs but isn’t measured. Always use self.client.
Blocking sync code blocks the whole worker. Greenlets are cooperative — a CPU-bound NumPy call or a time.sleep() pauses every other user on that process. Use gevent.sleep().
One node has a ceiling. A single Python process tops out around 1–3k VUs depending on workload. Hit the wall → --processes or distributed mode.

Go deeperSee also

Locust FAQ

What is Locust used for?

Locust is an open-source Python load testing tool. You write user behavior as plain Python code (not XML or YAML), then Locust spawns thousands of concurrent users to hammer your service and records response times, error rates, and throughput in real time via a web UI or headless mode.

What is the difference between HttpUser and FastHttpUser in Locust?

HttpUser uses the requests library under the hood — full feature parity but slower due to the synchronous I/O. FastHttpUser uses geventhttpclient, which is significantly faster for high-concurrency scenarios. Use FastHttpUser when you need to simulate thousands of users per machine; use HttpUser when you need requests-specific features like session or auth adapters.

How do I run Locust in headless mode?

Add --headless to skip the web UI: locust --headless -u 200 -r 20 -t 5m sets 200 users, a spawn rate of 20 users/second, and a 5-minute test duration. Pair with --csv=results to write output files, and hook events.quitting to set the exit code based on SLO thresholds.

What is a custom load shape in Locust?

A custom load shape lets you vary user count and spawn rate over time in arbitrary patterns — ramps, spikes, steps, or real-traffic replays. Subclass LoadTestShape and implement tick() to return (user_count, spawn_rate) tuples. Return None to stop the test. Shapes override the --users and --spawn-rate CLI flags.

How does distributed load testing work in Locust?

Run one master (locust --master) and any number of workers (locust --worker --master-host=master). Workers connect to the master, receive task instructions, and send results back for aggregation. Each worker can simulate hundreds of users, letting you scale to millions of requests per second across a cluster.

Is Locust free and open source?

Yes. Locust is MIT-licensed and free to use. It runs on any machine with Python installed. For large distributed tests you pay only for the infrastructure (VMs, containers) you run the workers on.