Locust describes virtual users in Python and measures a system under traffic. A useful scenario models real journeys and answers a question, such as when latency crosses a limit, rather than chasing the largest request count.

Create a locustfile

python -m pip install locust
from locust import HttpUser, between, task


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

    @task(3)
    def list_posts(self) -> None:
        self.client.get("/api/posts", name="GET /api/posts")

    @task
    def open_post(self) -> None:
        self.client.get("/api/posts/python", name="GET /api/posts/:slug")

Weights approximate action frequency, and wait_time prevents an unrealistic tight loop. Grouping dynamic URLs with name keeps reports readable.

Run safely and read results

Start with a few users and ramp gradually. Headless mode supports repeatable CI thresholds. Observe latency percentiles, failure rate, throughput, server resources, and the load generator itself.

Never target production without explicit authorization, an agreed window, limits, and a stop mechanism. Use dedicated test accounts and validate the journey functionally before applying load.

The Python API security guide covers risk controls, while Prometheus metrics helps observe the server.

The official Locust documentation, accessed July 22, 2026, covers HttpUser, tasks, headless execution, and distributed load. Record configuration, versions, data, and environment so results remain comparable.

Define the goal and load profile

Start with a measurable hypothesis: search must sustain 80 requests per second for ten minutes, with p95 below 400 ms and fewer than 1% failures. This defines the journey, duration, and acceptance rule. Distinguish load tests, which check expected traffic; stress tests, which find a limit; and endurance tests, which expose leaks or growing queues. Record the commit, infrastructure, data set, and generator configuration.

Model state and test data

Real journeys need authentication or existing data. on_start prepares each virtual user:

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

    def on_start(self) -> None:
        response = self.client.post(
            "/api/login",
            json={"email": "[email protected]", "password": "test-only-secret"},
            name="POST /api/login",
        )
        response.raise_for_status()
        self.token = response.json()["token"]

    @task
    def view_cart(self) -> None:
        self.client.get(
            "/api/cart",
            headers={"Authorization": f"Bearer {self.token}"},
            name="GET /api/cart",
        )

Use dedicated test credentials and supply secrets through environment variables. Do not share one account among thousands of users when real sessions are independent. Prepare enough identifiers and restore the database when a scenario mutates state.

Validate functional success

An API can return 200 with an incomplete body. catch_response classifies it according to the contract:

@task
def search(self) -> None:
    with self.client.get(
        "/api/search?q=python",
        name="GET /api/search",
        catch_response=True,
    ) as response:
        if response.status_code != 200:
            response.failure(f"HTTP {response.status_code}")
        elif "items" not in response.json():
            response.failure("response has no items")

Keep validation inexpensive so the generator does not become the bottleneck. Detailed rules belong in functional tests; under load, check only what distinguishes a genuine success.

Run headless with a ramp

locust -f locustfile.py \
  --headless \
  --host https://api.staging.example \
  --users 100 \
  --spawn-rate 5 \
  --run-time 10m \
  --csv results

--users means concurrent users, not requests per second. Throughput also depends on wait time and latency. Spawn rate controls the ramp, not the plateau. Run a small trial first to verify authentication, grouping, and cleanup. In CI, apply explicit limits and retain CSV output as evidence.

Interpret percentiles and bottlenecks

The median describes the middle request; p95 and p99 reveal the slow tail. Read them with throughput, failures, and per-endpoint counts. Correlate the same interval with CPU, memory, connection pools, queues, and database latency. If throughput stops increasing while generator CPU is saturated, the client may be the limit.

Create a baseline, change one variable, and repeat. Cache warming, autoscaling, and garbage collection create different phases. A reliable report preserves the timeline instead of selecting one favorable number.

Distribute and repeat

Locust coordinates a master and multiple workers when one machine is insufficient. Every process must run the same scenario version and use synchronized clocks. Keep generators separate from the measured infrastructure to avoid resource competition. Monitor their CPU and verify that DNS, load balancers, and network limits represent the intended path.

Before testing, document authorization, target, data, limits, and the emergency stop. During the run, observe service and generators. Afterwards, retain configuration, commit, timestamps, CSV files, and incidents. The result should drive a decision: accept a limit, investigate an endpoint, adjust capacity, or test a new hypothesis. It provides evidence for recorded conditions, not a guarantee that failure is impossible.

Avoid misleading conclusions

Do not merge fast and slow endpoints into one average. Analyze each operation and its share of the journey. Check whether authentication failures, rate limits, or cache hits artificially reduced server work. Thousands of fast 401 responses do not measure the protected operation's capacity.

Repeat the run to separate normal variation from regression. Report repetitions, the observed range, and changes between them. If a shared environment received unrelated traffic, record that limitation. Transparent uncertainty is more useful than false precision.

Keep units and report time zones explicit so another engineer can audit the evidence later.