TL;DR: Locust is a Python load testing framework. Write user scenarios as Python classes, run tests via web UI or CLI, scale with distributed mode. Perfect for Python teams wanting code-based performance tests with CI/CD integration.
Locust is a Python-based open-source load testing framework that enables engineers to define complex user behavior scenarios as Python code rather than XML configurations or GUI workflows. With over 24,000 GitHub stars and active maintenance, it has become the preferred load testing tool for Python-centric teams. According to the JetBrains Developer Ecosystem Survey 2024, Python is used by 51% of developers for test automation — making Locust a natural fit for teams already invested in the Python ecosystem. Unlike JMeter’s thread-based model, Locust uses greenlets (lightweight coroutines) enabling a single machine to simulate thousands of concurrent users with minimal memory. This comprehensive guide covers Locust from first test to distributed production load testing.
Introduction to Locust
Locust is a Python-based, open-source load testing tool that enables developers to write test scenarios in pure Python code. Unlike GUI-heavy tools like JMeter, Locust provides a code-first approach with powerful distributed testing capabilities and real-time web-based monitoring.
Locust fits naturally into continuous testing in DevOps workflows and complements API performance testing strategies. When building a comprehensive test automation strategy, Locust’s Python-based approach integrates seamlessly with CI/CD pipeline optimization tooling.
Basic Load Test
# locustfile.py
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 3) # Wait 1-3 seconds between tasks
host = "https://api.example.com"
@task(3) # Weight: 3x more likely than other tasks
def view_products(self):
self.client.get("/api/products")
@task(1)
def view_product_detail(self):
product_id = 123
self.client.get(f"/api/products/{product_id}")
@task(2)
def add_to_cart(self):
self.client.post("/api/cart", json={
"product_id": 456,
"quantity": 1
})
def on_start(self):
# Login before starting tasks
response = self.client.post("/api/auth/login", json={
"username": "test@example.com",
"password": "password123"
})
self.token = response.json()["token"]
self.client.headers.update({"Authorization": f"Bearer {self.token}"})
Advanced Scenarios
Sequential User Flow
from locust import HttpUser, task, SequentialTaskSet, between
class UserBehavior(SequentialTaskSet):
def on_start(self):
self.product_id = None
self.cart_id = None
@task
def browse_homepage(self):
self.client.get("/")
@task
def search_products(self):
response = self.client.get("/api/products?category=electronics")
products = response.json()
if products:
self.product_id = products[0]["id"]
@task
def view_product(self):
if self.product_id:
self.client.get(f"/api/products/{self.product_id}")
@task
def add_to_cart(self):
if self.product_id:
response = self.client.post("/api/cart", json={
"product_id": self.product_id,
"quantity": 1
})
self.cart_id = response.json()["cart_id"]
@task
def checkout(self):
if self.cart_id:
self.client.post(f"/api/checkout/{self.cart_id}", json={
"payment_method": "credit_card",
"shipping_address": "123 Main St"
})
@task
def stop(self):
self.interrupt() # Stop task sequence
class EcommerceUser(HttpUser):
wait_time = between(2, 5)
tasks = [UserBehavior]
Data-Driven Testing
import csv
from locust import HttpUser, task, between
from itertools import cycle
class DataDrivenUser(HttpUser):
wait_time = between(1, 2)
def on_start(self):
# Load test data
with open('test_users.csv', 'r') as f:
reader = csv.DictReader(f)
self.user_data = cycle(list(reader))
@task
def login_with_test_data(self):
user = next(self.user_data)
response = self.client.post("/api/login", json={
"username": user['username'],
"password": user['password']
})
if response.status_code == 200:
self.token = response.json()["token"]
Distributed Load Testing
Master Configuration
# Run master node
# locust -f locustfile.py --master --expect-workers=4
# Run worker nodes
# locust -f locustfile.py --worker --master-host=localhost
Docker Compose Setup
# docker-compose.yml
version: '3'
services:
master:
image: locustio/locust
ports:
- "8089:8089"
volumes:
- ./:/mnt/locust
command: -f /mnt/locust/locustfile.py --master --expect-workers=4
worker:
image: locustio/locust
volumes:
- ./:/mnt/locust
command: -f /mnt/locust/locustfile.py --worker --master-host=master
deploy:
replicas: 4
Kubernetes Deployment
# locust-master.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust-master
spec:
replicas: 1
template:
spec:
containers:
- name: locust
image: locustio/locust
args: ["-f", "/locust/locustfile.py", "--master"]
ports:
- containerPort: 8089
- containerPort: 5557
volumeMounts:
- name: locust-scripts
mountPath: /locust
---
# locust-worker.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust-worker
spec:
replicas: 10
template:
spec:
containers:
- name: locust
image: locustio/locust
args: ["-f", "/locust/locustfile.py", "--worker", "--master-host=locust-master"]
volumeMounts:
- name: locust-scripts
mountPath: /locust
Custom Metrics and Reporting
from locust import HttpUser, task, events
import time
# Custom metric tracking
request_times = []
@events.request.add_listener
def on_request(request_type, name, response_time, response_length, exception, **kwargs):
request_times.append(response_time)
if response_time > 1000: # Alert on slow requests
print(f"Slow request detected: {name} took {response_time}ms")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
# Calculate percentiles
if request_times:
sorted_times = sorted(request_times)
p50 = sorted_times[len(sorted_times) // 2]
p95 = sorted_times[int(len(sorted_times) * 0.95)]
p99 = sorted_times[int(len(sorted_times) * 0.99)]
print(f"\nPerformance Summary:")
print(f"P50: {p50}ms")
print(f"P95: {p95}ms")
print(f"P99: {p99}ms")
class MonitoredUser(HttpUser):
@task
def monitored_request(self):
start_time = time.time()
response = self.client.get("/api/data")
elapsed = (time.time() - start_time) * 1000
# Custom validation
if response.status_code == 200:
data = response.json()
if len(data) < 10:
events.request.fire(
request_type="GET",
name="/api/data",
response_time=elapsed,
response_length=len(response.content),
exception=Exception("Insufficient data returned")
)
Locust vs JMeter Comparison
| Feature | Locust | JMeter |
|---|---|---|
| Language | Python | GUI/XML |
| Learning Curve | Low (if Python known) | Medium-High |
| Test as Code | Yes (native) | Limited (requires plugins) |
| Distributed Testing | Built-in | Built-in |
| Real-time UI | Web-based, modern | Java Swing, dated |
| Version Control | Excellent (Python files) | Poor (XML files) |
| Resource Usage | Lower | Higher (Java) |
| Protocol Support | HTTP/WebSocket (extensible) | Extensive built-in |
| CI/CD Integration | Excellent | Good |
| Scripting Flexibility | Excellent (Python) | Limited (Groovy/BeanShell) |
| Community | Growing | Large, established |
| License | MIT | Apache 2.0 |
CI/CD Integration
GitHub Actions
# .github/workflows/load-test.yml
name: Load Test
on:
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
workflow_dispatch:
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install locust
- name: Run Load Test
run: |
locust -f locustfile.py \
--headless \
--users 100 \
--spawn-rate 10 \
--run-time 5m \
--host https://api.example.com \
--html report.html \
--csv results
- name: Upload Results
uses: actions/upload-artifact@v3
with:
name: load-test-results
path: |
report.html
results_*.csv
- name: Check Performance Thresholds
run: |
python check_thresholds.py results_stats.csv
Threshold Validation Script
# check_thresholds.py
import csv
import sys
def check_thresholds(stats_file):
thresholds = {
'avg_response_time': 200, # ms
'max_response_time': 2000, # ms
'failure_rate': 0.01 # 1%
}
with open(stats_file, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
if row['Name'] == 'Aggregated':
avg_response = float(row['Average Response Time'])
max_response = float(row['Max Response Time'])
failure_rate = float(row['Failure Count']) / float(row['Request Count'])
if avg_response > thresholds['avg_response_time']:
print(f"FAIL: Average response time {avg_response}ms exceeds threshold")
sys.exit(1)
if max_response > thresholds['max_response_time']:
print(f"FAIL: Max response time {max_response}ms exceeds threshold")
sys.exit(1)
if failure_rate > thresholds['failure_rate']:
print(f"FAIL: Failure rate {failure_rate:.2%} exceeds threshold")
sys.exit(1)
print("PASS: All thresholds met")
sys.exit(0)
if __name__ == '__main__':
check_thresholds(sys.argv[1])
Best Practices
1. Realistic User Simulation
from locust import HttpUser, task, between
import random
class RealisticUser(HttpUser):
wait_time = between(1, 5)
@task(10)
def browse(self):
# Simulate browsing behavior
pages = ['/products', '/about', '/contact']
self.client.get(random.choice(pages))
@task(3)
def search(self):
keywords = ['laptop', 'phone', 'tablet', 'watch']
self.client.get(f"/search?q={random.choice(keywords)}")
@task(1)
def purchase(self):
# Only 10% of users purchase
if random.random() < 0.1:
self.client.post("/api/orders", json={
"items": [{"id": random.randint(1, 100), "qty": 1}]
})
2. Proper Error Handling
from locust import HttpUser, task
from locust.exception import RescheduleTask
class ResilientUser(HttpUser):
@task
def api_call(self):
with self.client.get("/api/data", catch_response=True) as response:
if response.status_code == 429: # Rate limited
response.failure("Rate limited")
raise RescheduleTask() # Retry later
elif response.status_code == 500:
response.failure("Server error")
elif response.elapsed.total_seconds() > 2:
response.failure("Request too slow")
else:
response.success()
3. Environment-Specific Configuration
import os
from locust import HttpUser, task
class ConfigurableUser(HttpUser):
host = os.getenv("TARGET_HOST", "http://localhost:3000")
wait_time = between(
int(os.getenv("MIN_WAIT", 1)),
int(os.getenv("MAX_WAIT", 3))
)
@task
def make_request(self):
self.client.get(os.getenv("ENDPOINT", "/api/data"))
Conclusion
Locust provides a modern, Python-based approach to load testing that excels in code-first workflows, version control integration, and distributed testing. Its simplicity and flexibility make it ideal for developers familiar with Python who want to define performance tests as code.
Choose Locust when:
- Team is comfortable with Python
- Tests as code is preferred
- Modern, web-based UI desired
- Easy version control needed
- Distributed testing required
Choose JMeter when:
- Extensive protocol support needed
- Team prefers GUI-based test creation
- Large existing JMeter infrastructure
- Non-HTTP protocols heavily used
For modern development teams practicing DevOps and infrastructure as code, Locust offers the perfect blend of simplicity, power, and maintainability for load testing.
Official Resources
“What I love about Locust is that the test IS the documentation. A well-written locustfile shows you exactly what user workflows the system was tested against. That clarity is invaluable when a performance regression appears six months later.” — Yuri Kan, Senior QA Lead
FAQ
What is Locust and how does it work?
Locust is a Python load testing framework where user behavior is defined as Python classes with task methods executed concurrently via greenlets.
Locust uses Python’s gevent library (greenlets) for concurrency rather than threads, allowing thousands of virtual users per process with minimal overhead. You define a User class with @task-decorated methods representing user actions. Locust randomly selects and executes tasks based on their weight, simulating realistic user behavior patterns. The web UI shows real-time metrics; CLI mode enables headless execution in CI/CD.
How does Locust compare to JMeter?
Locust uses Python code for readable, versionable tests; JMeter uses XML with GUI. Locust wins on developer experience; JMeter on protocol variety.
Locust tests are Python classes — readable in PRs, easy to parameterize with Python logic, and straightforward to maintain. JMeter tests are XML (hard to review) with a GUI (not CI/CD friendly). Locust excels for HTTP API testing with complex user simulation logic. JMeter supports more protocols (JDBC, JMS, FTP, SMTP) and has a larger enterprise ecosystem. Performance-wise, both handle similar loads per machine.
How do I run distributed Locust tests?
Start one master process and multiple worker processes. Workers connect to master and receive user simulation tasks automatically.
Distributed Locust setup: start master with locust --master -f locustfile.py, start workers on other machines with locust --worker --master-host=MASTER_IP -f locustfile.py. The master distributes users evenly across workers. For cloud-based distributed testing, use Docker with a master service and scaled worker services, or Kubernetes with a master deployment and worker deployment with replicas: N.
What metrics does Locust report?
Requests/s, response time percentiles (median, 95th), failure rate, and user count. Export to CSV or integrate with Grafana for dashboards.
Locust reports per-endpoint and aggregate metrics: request count, failure count, response time (median, 90th, 95th, 99th percentiles), requests/s, and failures/s. The web UI shows real-time charts. Use --csv=results to export summary and per-request CSVs. For production monitoring dashboards, the locust-grafana plugin sends metrics to InfluxDB for Grafana visualization.
See Also
- API Performance Testing
- K6: Modern Load Testing with JavaScript for DevOps Teams
- Nightwatch.js E2E Testing: Complete Guide to Node.js Browser Automation - Comprehensive Nightwatch.js guide covering page objects, custom commands,…
- Master K6 for modern performance testing: JavaScript-based load testing, CI/CD…
- Comprehensive strategies for API load testing
- Continuous Testing in DevOps - Integrating performance testing into CI/CD workflows
- CI/CD Pipeline Optimization for QA Teams - Automating Locust tests in deployment pipelines
- Test Automation Strategy - Building performance testing into your automation framework
- Containerization for Testing - Running distributed Locust tests in containers
