← Back to DevBytes

How to Set Up a Load Tester with Locust.io

What Is Locust?

Locust is an open-source load testing tool written in Python that lets you define user behavior with plain code, then swarm your application with thousands of concurrent users to measure performance. Unlike traditional tools that rely on clunky GUIs or XML configuration files, Locust gives you a scriptable, developer-friendly interface where test scenarios are simply Python functions decorated with lightweight directives.

The key architectural difference from tools like JMeter or Gatling is that Locust is event-driven and runs on gevent, a coroutine-based networking library. This means a single process can handle thousands of simultaneous virtual users without spawning a thread or process per user, keeping memory overhead remarkably low. You write a locustfile.py, point it at a target, and let the swarm begin.

Why Load Testing Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Every production system has a breaking point. The question is whether that breaking point sits above or below your expected traffic. Load testing answers this before your users do. Specifically, load testing with Locust helps you:

Load testing is not a one-time activity; it belongs in your CI/CD pipeline as a regression guard. Every significant code change should be validated against a performance baseline to catch regressions early.

Installation and Project Setup

Install Locust via pip. It is recommended to use a virtual environment to keep dependencies isolated:

# Create and activate a virtual environment
python -m venv locust-env
source locust-env/bin/activate  # On Windows: locust-env\Scripts\activate

# Install Locust
pip install locust

Verify the installation by checking the available CLI commands:

locust --version
# Should output something like: locust 2.31.0

That is all the setup required. Locust does not need a daemon process, a database, or any infrastructure beyond Python itself. Your test definitions live in a file that you create from scratch.

Writing Your First Locustfile

A locustfile is where you define user behavior. At minimum it needs two things: a class that inherits from User (or HttpUser for HTTP testing), and at least one method decorated with @task.

Create a file called locustfile.py in your working directory:

from locust import HttpUser, task, between
import random

class MyApiUser(HttpUser):
    # Wait 1 to 3 seconds between each simulated user's consecutive tasks
    wait_time = between(1, 3)

    @task(3)
    def get_homepage(self):
        """Weight of 3 — this task runs 3x more often than the others."""
        self.client.get("/")

    @task(1)
    def get_user_by_id(self):
        user_id = random.randint(1, 100)
        self.client.get(f"/users/{user_id}", name="/users/{id}")

    @task(2)
    def create_user(self):
        payload = {
            "name": "locust_user",
            "email": "locust@example.com"
        }
        # catch_response=True lets you mark success/failure manually
        with self.client.post("/users", json=payload, catch_response=True) as response:
            if response.status_code == 201:
                response.success()
            else:
                response.failure(f"Unexpected status code: {response.status_code}")

Understanding the Components

Running the Load Test

Start Locust from the command line. Without any arguments, it launches the web UI on port 8089:

locust -f locustfile.py

Open http://localhost:8089 in your browser. You will see a clean dashboard where you enter:

Click "Start swarming" and the test begins. The web UI provides real-time charts for request rate, response times, and failure count.

Headless Mode (for CI/CD)

For automated pipelines, run Locust without the web UI using the --headless flag:

locust -f locustfile.py \
  --headless \
  --host https://staging.example.com \
  --users 500 \
  --spawn-rate 50 \
  --run-time 10m \
  --csv results \
  --html report.html

Key flags explained:

The CSV output is parseable by any scripting language, making it easy to gate deployments based on threshold assertions (e.g., "fail the pipeline if p95 latency exceeds 200ms").

Advanced Locustfile Patterns

Sequential Task Chains

Sometimes a user must follow a specific flow — login, then browse, then checkout. Use SequentialTaskSet to enforce ordering:

from locust import HttpUser, SequentialTaskSet, task, between

class CheckoutFlow(SequentialTaskSet):
    @task
    def login(self):
        self.client.post("/login", json={"user": "test", "pass": "test"})

    @task
    def add_to_cart(self):
        self.client.post("/cart/items", json={"product_id": 42, "quantity": 1})

    @task
    def checkout(self):
        self.client.post("/checkout", json={"cart_id": "abc"})

    @task
    def logout(self):
        self.client.post("/logout")

class ShoppingUser(HttpUser):
    wait_time = between(2, 5)
    tasks = [CheckoutFlow]  # Assign the SequentialTaskSet as the task container

Each virtual user now runs login → add_to_cart → checkout → logout in strict order, then loops back to login. The wait_time applies between each step.

Custom Client for Non-HTTP Protocols

Locust is not limited to HTTP. You can test any protocol by extending the base User class and writing a custom client. Here is an example testing a gRPC service:

import grpc
from locust import User, task, between
import my_service_pb2
import my_service_pb2_grpc

class GrpcClient:
    def __init__(self, host):
        self.channel = grpc.insecure_channel(host)
        self.stub = my_service_pb2_grpc.MyServiceStub(self.channel)

    def call_get_item(self, item_id):
        request = my_service_pb2.GetItemRequest(id=item_id)
        return self.stub.GetItem(request, timeout=2)

class GrpcUser(User):
    wait_time = between(0.5, 1.5)

    def on_start(self):
        # Called once when a user starts
        self.client = GrpcClient("grpc-backend:50051")

    @task
    def get_item(self):
        try:
            response = self.client.call_get_item(42)
            # Record success in Locust's statistics
            self.environment.events.request_success.fire(
                request_type="grpc",
                name="GetItem",
                response_time=0.0,  # measure manually if needed
                response_length=len(response.SerializeToString()),
            )
        except Exception as e:
            self.environment.events.request_failure.fire(
                request_type="grpc",
                name="GetItem",
                response_time=0.0,
                exception=e,
            )

This pattern works for WebSockets, Kafka producers, TCP sockets, or any network protocol. The key is firing request_success and request_failure events so Locust aggregates statistics properly.

Dynamic Data Feeds

Hard-coded values make for unrealistic traffic. Use Python generators or external data sources to feed varied inputs:

import csv
from locust import HttpUser, task, between

# Load a pool of user credentials once at module load time
def load_credentials():
    with open("users.csv") as f:
        return list(csv.DictReader(f))

CREDENTIALS = load_credentials()

class AuthenticatedUser(HttpUser):
    wait_time = between(5, 15)

    def on_start(self):
        # Each virtual user picks a random credential set
        creds = random.choice(CREDENTIALS)
        resp = self.client.post("/auth/token", json=creds)
        self.token = resp.json()["access_token"]
        self.client.headers["Authorization"] = f"Bearer {self.token}"

    @task
    def view_dashboard(self):
        self.client.get("/dashboard")

This ensures each simulated user has unique authentication data, mimicking real-world session diversity.

Running Distributed Load Tests

A single Locust process can typically generate 5,000–10,000 concurrent users on modern hardware. When you need more, run in distributed mode with a master-worker architecture:

# On the master node (runs the web UI and coordinates workers)
locust -f locustfile.py --master --expect-workers 4

# On each worker node (runs the actual load generation)
locust -f locustfile.py --worker --master-host 192.168.1.10

The master distributes spawn commands and aggregates statistics. Workers must have identical locustfiles and reachable network connectivity to the master. Use --expect-workers to block the test from starting until the specified number of workers have connected, preventing premature ramp-up.

For Kubernetes deployments, consider the official Locust Helm chart or run workers as separate pods with the master service exposed via a ClusterIP.

Best Practices

1. Run Tests from a Staging Environment

Never run a full-scale load test against production unless you have absolute confidence in your system's resilience and have coordinated with your operations team. Even then, start with a smoke test at 1% of expected load. Use a dedicated staging environment that mirrors production infrastructure as closely as possible — same database instance class, same network topology, same load balancer configuration.

2. Warm Up the Target System

Before collecting metrics, let the system serve traffic for a few minutes. Cold caches, lazy-loaded modules, and uninitialized connection pools produce misleadingly slow early measurements. Use the --run-time flag generously (at least 5–10 minutes for meaningful data) and discard the first 60 seconds when analyzing results.

3. Watch the Client-Side Resource Usage

If your Locust worker CPU hits 100%, it becomes the bottleneck rather than the target system. Monitor worker CPU, memory, and open file descriptors. Increase the number of worker nodes or reduce the logging level (--log-level ERROR) if you observe client-side saturation. A good rule of thumb: keep worker CPU below 80% to leave headroom for gevent's event loop.

4. Use Constant Pacing for Rate-Based Tests

between() introduces randomness that makes it hard to reason about exact throughput. When you need to hit a specific request rate, use constant_pacing:

wait_time = constant_pacing(2)  # Exactly one task every 2 seconds per user

With 500 users and 2-second pacing, you achieve exactly 250 requests per second across all tasks, which makes capacity planning straightforward.

5. Group Endpoints with the name Parameter

Always pass the name argument to self.client.get() and self.client.post() when URLs contain dynamic segments. Without it, Locust creates a separate statistics bucket for every unique URL, making the results page unusable and memory-intensive.

6. Implement Custom Success/Failure Logic

HTTP status codes alone do not tell the full story. A 200 OK response containing an error message body is a failure. Use catch_response=True and validate the response payload:

with self.client.post("/api/orders", json=order, catch_response=True) as resp:
    if resp.status_code == 200 and resp.json().get("status") == "confirmed":
        resp.success()
    else:
        resp.failure("Order not confirmed in response body")

7. Version Your Locustfiles

Store your locustfile in the same repository as the application code it tests. Tag releases so you can reproduce load tests against historical versions. A locustfile from six months ago may not work against today's API, but it should remain available for auditing and comparison.

8. Integrate with Monitoring Tools

Locust exposes a Prometheus-compatible metrics endpoint via the --web-port and an optional plugin. Combine Locust metrics with server-side observability (CPU, memory, DB query latency) to correlate load with infrastructure behavior. A load test without server-side data tells you that something is slow, but not why.

Interpreting Results

After a run, Locust presents several key metrics:

A common interpretation workflow:

  1. Check failures first — any non-zero failure rate demands immediate investigation
  2. Look at p95/p99 latency at your target concurrency — this is what real users experience
  3. Correlate latency spikes with user count increases — does performance degrade linearly or exhibit a cliff?
  4. Compare throughput against your infrastructure's theoretical limits
  5. Re-run with a slightly higher user count to find the actual breaking point

Conclusion

Locust gives you a powerful, code-first approach to load testing that fits naturally into a developer's workflow. You define user behavior in Python, run tests interactively through a web UI or headless in CI/CD pipelines, and collect actionable performance data. The event-driven architecture lets a modest machine simulate thousands of concurrent users, while the extensible design supports HTTP, gRPC, WebSocket, and custom protocols through the same unified interface.

Start by writing a small locustfile that hits your most critical endpoints, run it regularly against a staging environment, and gradually build up a suite of scenarios that mirrors real user journeys. Treat performance as a first-class quality signal, gated alongside unit tests and integration tests in your deployment pipeline. The earlier you catch a latency regression, the cheaper it is to fix.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles