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:
- Identify bottlenecks — discover slow database queries, blocking I/O, or CPU-bound endpoints under concurrency
- Establish capacity baselines — know exactly how many concurrent users each service tier can handle before latency degrades
- Validate autoscaling — confirm that your cloud infrastructure scales out (and back in) within acceptable timeframes
- Detect race conditions — concurrent requests expose locking issues that sequential unit tests never catch
- Set realistic SLIs — define SLOs for p95/p99 latency based on empirical data rather than guesswork
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
HttpUser— the base class for HTTP testing. It gives each virtual user aself.clientattribute, which is a pre-configuredrequests.Session-like object (actually a faster implementation) that automatically handles cookies and connection pooling.wait_time— controls the pause between tasks.between(low, high)randomizes the wait uniformly. Alternatives includeconstant(seconds)andconstant_pacing(seconds)(which enforces a fixed total cycle time regardless of task duration).@task— marks a method as a callable action. The optional integer argument sets the weight; a task with weight 3 is executed three times as often as a task with weight 1. If omitted, weight defaults to 1.nameparameter — grouping parameter. When you call/users/{id}with varying IDs, use thenamekwarg so Locust aggregates all those requests under a single statistics entry rather than scattering them across thousands of unique URLs.catch_response— by default Locust considers any HTTP 2xx response a success. Settingcatch_response=Truegives you manual control over success/failure classification.
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:
- Number of users — peak concurrency (spawn count)
- Spawn rate — how many users to hatch per second
- Host — the base URL of the target system (e.g.,
https://staging.example.com)
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:
--run-time— duration of the test (e.g.,60s,10m,1h). Locust stops automatically after this period.--csv— prefix for CSV output files. Produces three files:results_stats.csv,results_failures.csv, andresults_exceptions.csv.--html— generates a standalone HTML report with charts and summary statistics, ideal for archiving or sharing results.
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:
- Requests/sec — total throughput the system handled successfully
- Response time percentiles — median, p95, p99, and max latency in milliseconds
- Failure count and rate — absolute number and percentage of failed requests
- User count over time — the concurrency ramp-up curve
A common interpretation workflow:
- Check failures first — any non-zero failure rate demands immediate investigation
- Look at p95/p99 latency at your target concurrency — this is what real users experience
- Correlate latency spikes with user count increases — does performance degrade linearly or exhibit a cliff?
- Compare throughput against your infrastructure's theoretical limits
- 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.