A powerful, YAML-based load testing framework built on Locust. Define complex API test scenarios with multi-user authentication, data generation, encryption, and validation - all through simple configuration files.
- Why We Built This
- Features
- Quick Start
- Installation
- Configuration Reference
- Plugins
- Validation
- Advanced Usage
- Command Reference
- Examples
- Troubleshooting
- Contributing
Writing Locust tests usually means writing Python — and that's fine for one project. But once you're juggling many services, many environments, and many teams, it quickly becomes painful.
Every project ends up with:
- Slightly different Locust scripts
- Repeated boilerplate
- Copy-paste logic with tiny changes
- Yet another Python file to maintain
At some point the question becomes:
"Why am I rewriting Python just to describe HTTP flows?"
Locust Flow was built to fix that.
Instead of treating load tests as code, it treats them as configuration:
- API flows belong in YAML/JSON
- Logic should be reusable, not rewritten
- Adding a new service should not mean adding new Python files
The goal is simple: define traffic once, reuse it everywhere — without the mental overhead of maintaining Python test code across dozens of projects.
If you've ever felt annoyed writing Python just to load test another API, Locust Flow is for you.
service_name: "E-commerce API"
base_url: "https://api.shop.com"
run_init_once: true
init_list_var: "users"
variables:
users:
- "user001"
- "user002"
- "user003"
- "user004"
- "user005"
passwords:
- "pass001"
- "pass002"
- "pass003"
- "pass004"
- "pass005"
init:
- name: "Login"
method: "POST"
endpoint: "/auth/login"
pre_transforms:
- type: "select_from_list"
config:
from: "users"
mode: "round_robin"
output: "username"
- type: "select_from_list"
config:
from: "passwords"
mode: "round_robin"
output: "password"
data:
username: "{{ username }}"
password: "{{ password }}"
extract:
token: "json.access_token"
post_transforms:
- type: "store_data"
config:
key: "{{ username }}"
values:
- "token"
steps:
- name: "Browse Products"
weight: 0.5
method: "GET"
endpoint: "/products"
headers:
Authorization: "Bearer {{ token }}"
- name: "Add to Cart"
weight: 0.3
method: "POST"
endpoint: "/cart"
headers:
Authorization: "Bearer {{ token }}"
Content-Type: "application/x-www-form-urlencoded"
data:
product_id: "{{ product_id }}"
quantity: "{{ quantity }}"
pre_transforms:
- type: "random_number"
config:
min: 1
max: 5
output: "quantity"
- name: "Checkout"
weight: 0.2
method: "POST"
endpoint: "/checkout"
headers:
Authorization: "Bearer {{ token }}"
validate:
status_code: 200
max_response_time: 3000Run it:
make runWhat happens:
- Validates config automatically
- Logs in 5 users once at startup
- 100 virtual users make requests using those 5 accounts
- 50% browse, 30% add to cart, 20% checkout
- Random quantities, validated responses
- YAML Configuration - Define tests without writing code
- Multi-User Testing - Simulate realistic load with multiple accounts
- Data Generation - Random numbers, UUIDs, timestamps, encryption
- Response Validation - Automated assertions on status, timing, and content
- Conditional Logic - Skip steps based on runtime conditions
- Variable Extraction - Chain requests by extracting and reusing data
- Retry Handling - Automatic retries for transient failures
# Create virtual environment
make venv
# Activate it
source .venv/bin/activate # Linux/Mac
.venv\Scripts\activate # WindowsWhy use a virtual environment?
- Isolates project dependencies
- Prevents conflicts with system packages
- Easy to recreate and share
make installmake validate-configs# Web UI (recommended)
make run
# Headless mode
make run-headlessWhy use make?
| Benefit | Description |
|---|---|
| Pre-validation | Validates configs before running, prevents runtime errors |
| Consistency | Same commands work across all environments |
| Error Handling | Automatic error detection and reporting |
| Best Practices | Built-in optimizations and safety checks |
# Only if you can't use make
# 1. Create virtual environment
python3 -m venv .venv
source .venv/bin/activate # Linux/Mac
.venv\Scripts\activate # Windows
# 2. Install dependencies
pip install -r requirements.txt
# 3. Validate configs
python validate_config.py configs/*.yaml
# 4. Run Locust
locust -f main.py
# Open http://localhost:8089service_name: "My API"
base_url: "https://api.example.com"
run_init_once: true
init_list_var: "users"
variables:
users:
- "user1"
- "user2"
api_key: "abc123"
init:
- name: "Login"
method: "POST"
endpoint: "/auth/login"
extract:
token: "json.token"
steps:
- name: "Get Data"
weight: 1.0
method: "GET"
endpoint: "/data"
headers:
Authorization: "Bearer {{ token }}"Important: When using the data field for form data, you must include a Content-Type header (e.g., application/x-www-form-urlencoded or application/json).
Control throughput and wait times per service:
service_name: "My API"
base_url: "https://api.example.com"
# Optional: Configure Locust behavior
locust:
wait_time: "constant_throughput" # Options: constant_throughput, constant, between, constant_pacing
throughput: 5 # 5 requests per second per user
steps:
- name: "API Call"
method: "GET"
endpoint: "/data"Available Options:
| wait_time | Required Fields | Description | Example |
|---|---|---|---|
constant_throughput |
throughput |
Fixed requests/sec per user | throughput: 5 = 5 req/s |
constant |
min_wait |
Fixed wait time (seconds) | min_wait: 2 = 2s wait |
between |
min_wait, max_wait |
Random wait time (seconds) | min_wait: 1, max_wait: 3 |
constant_pacing |
pacing |
Task runs every X seconds | pacing: 5 = every 5s |
Examples:
# High throughput - 10 requests per second per user
locust:
wait_time: "constant_throughput"
throughput: 10
# Fixed 2 second wait between requests
locust:
wait_time: "constant"
min_wait: 2
# Random wait between 1-5 seconds
locust:
wait_time: "between"
min_wait: 1
max_wait: 5
# Ensure task runs every 3 seconds
locust:
wait_time: "constant_pacing"
pacing: 3Note: If not specified, defaults to constant_throughput with 1 request/second per user.
The flow_init section runs once per virtual user after initialization, allowing you to select credentials that persist throughout the user's session.
run_init_once: true
init_list_var: "users"
variables:
users:
- "user001"
- "user002"
- "user003"
registered_user_keys: [] # Populated during init
init:
- name: "Login"
method: "POST"
endpoint: "/auth/login"
headers:
Content-Type: "application/x-www-form-urlencoded"
data:
username: "{{ user }}" # user comes from init_list_var
extract:
token: "json.token"
post_transforms:
# Store credentials
- type: "store_data"
config:
key: "user_{{ user }}"
values:
- "user"
- "token"
# Add to list of registered users
- type: "append_to_list"
config:
list_var: "registered_user_keys"
value: "user_{{ user }}"
# Select credentials once per virtual user
flow_init:
- type: "select_from_list"
config:
from: "registered_user_keys"
mode: "random"
output: "selected_user_key"
- type: "lookup_all"
config:
store_key: "{{ selected_user_key }}"
output: "user_creds"
steps:
- name: "Get Profile"
method: "GET"
endpoint: "/profile"
headers:
Authorization: "Bearer {{ user_creds.token }}"Benefits:
- ✅ Credentials selected once per virtual user (not per step)
- ✅ Consistent user behavior throughout the session
- ✅ No repetitive lookups in every step
- ✅ Better performance and cleaner configuration
| Plugin | Description | Configuration | Output |
|---|---|---|---|
random_number |
Generate random integer | min, max |
Integer between min and max |
random_string |
Generate random string | length |
Alphanumeric string |
random_choice |
Pick random item | choices (array) |
One item from array |
uuid |
Generate UUID v4 | None | UUID string |
timestamp |
Current timestamp | format (unix/iso) |
Timestamp string |
increment |
Auto-increment counter | start, step |
Incremented number |
| Plugin | Description | Configuration | Output |
|---|---|---|---|
select_from_list |
Pick from variable list | from, mode (round_robin/random) |
Selected item |
| Plugin | Description | Configuration | Input Required |
|---|---|---|---|
rsa_encrypt |
RSA encryption | public_key |
Data to encrypt |
sha256 |
SHA-256 hash | None | Data to hash |
hmac |
HMAC signature | key, algorithm |
Data to sign |
base64_encode |
Base64 encoding | None | Data to encode |
base64_decode |
Base64 decoding | None | Data to decode |
Store and retrieve data across virtual users with a shared data store.
| Plugin | Description | Configuration | Purpose |
|---|---|---|---|
store_data |
Store variables by key | key, values, refresh (optional) |
Multi-user token management |
lookup |
Retrieve single field | store_key, field |
Get specific stored value |
lookup_all |
Retrieve all fields | store_key |
Get all stored data for a key |
get_store_keys |
Get all stored keys | None | List all available keys |
append_to_list |
Append to list variable | list_var, value |
Build dynamic lists |
Key Behaviors:
- Same key = Merge: Multiple calls with the same key add fields without removing existing ones
- Thread-safe: Uses locks to prevent race conditions
- Persistent: Data persists across all virtual users during the test
- Refresh:
store_datawithrefresh: true(default) returns all stored data for the key
Example - Store and Retrieve Data:
# Store data with refresh (returns all stored data)
post_transforms:
- type: "store_data"
config:
key: "user_{{ user_id }}"
values:
- "token"
- "device_id"
refresh: true # Default: true, returns all stored data
output: "user_data" # Receives {token: "abc", device_id: "xyz"}
# Lookup single field
pre_transforms:
- type: "lookup"
config:
store_key: "user_102"
field: "token"
output: "auth_token"
# Lookup all fields
pre_transforms:
- type: "lookup_all"
config:
store_key: "user_102"
output: "user_data" # Gets {token: "abc", device_id: "xyz", ...}
# Get all stored keys
pre_transforms:
- type: "get_store_keys"
output: "all_user_keys" # Gets ["user_102", "user_103", ...]
# Build a list dynamically
post_transforms:
- type: "append_to_list"
config:
list_var: "registered_users"
value: "user_{{ user_id }}"Other Plugin Examples:
pre_transforms:
- type: "random_number"
config:
min: 100
max: 5000
output: "amount"
- type: "uuid"
output: "request_id"
- type: "rsa_encrypt"
input: "{{ password }}"
output: "encrypted_password"variables:
api_key: "abc123"
headers:
Authorization: "{{ api_key }}"
extract:
token: "json.access_token"
user_id: "json.user.id"
cookie: "headers.Set-Cookie"| Field | Type | Description | Example |
|---|---|---|---|
status_code |
Integer | Expected HTTP status | 200, 201, 404 |
max_response_time |
Integer | Max time in milliseconds | 2000, 5000 |
json |
Object | JSON path assertions | success: true |
fail_on_error |
Boolean | Stop test on failure | true, false |
| Field | Type | Description | Example |
|---|---|---|---|
field |
String | JSON path to validate | data.status, user.id |
condition |
String | Comparison operator | equals, contains, greater_than |
expected |
Any | Expected value | "active", 100, true |
| Condition | Description | Example |
|---|---|---|
equals |
Exact match | field: "status", expected: "success" |
not_equals |
Not equal | field: "error", expected: null |
contains |
String contains | field: "message", expected: "approved" |
not_contains |
String doesn't contain | field: "message", expected: "error" |
greater_than |
Numeric comparison | field: "balance", expected: 0 |
less_than |
Numeric comparison | field: "count", expected: 100 |
is_empty |
Check if empty | field: "errors" |
is_not_empty |
Check if not empty | field: "data" |
# Basic validation
validate:
status_code: 200
max_response_time: 2000
json:
success: true
data.status: "completed"
# Field-based validation
validate:
- field: "data.balance"
condition: "greater_than"
expected: 0
- field: "data.status"
condition: "equals"
expected: "active"
- field: "errors"
condition: "is_empty"steps:
- name: "Transfer Money"
skip_if:
condition: "equals"
left: "{{ sender }}"
right: "{{ receiver }}"
data:
from: "{{ sender }}"
to: "{{ receiver }}"| Condition | Description | Use Case |
|---|---|---|
equals |
Values are equal | Skip if sender equals receiver |
not_equals |
Values are different | Skip if status is not active |
contains |
String contains substring | Skip if message contains error |
greater_than |
Numeric comparison | Skip if balance too low |
less_than |
Numeric comparison | Skip if count exceeds limit |
is_empty |
Value is empty/null | Skip if no data available |
is_not_empty |
Value exists | Skip if already processed |
steps:
- name: "Browse"
weight: 0.5
- name: "Add to Cart"
weight: 0.3
- name: "Checkout"
weight: 0.2| Field | Type | Required | Description |
|---|---|---|---|
service_name |
String | Yes | Name of the service being tested |
base_url |
String | Yes | Base URL for all requests |
run_init_once |
Boolean | No | Run init steps once at startup |
init_list_var |
String | No | Variable containing user list |
variables |
Object | No | Global variables |
init |
Array | No | Initialization steps |
flow_init |
Array | No | Per-virtual-user initialization transforms |
steps |
Array | Yes | Main test steps |
| Field | Type | Required | Description |
|---|---|---|---|
name |
String | Yes | Step name for logging |
method |
String | Yes | HTTP method (GET, POST, PUT, DELETE, PATCH) |
endpoint |
String | Yes | API endpoint path |
weight |
Float | No | Execution probability (0.0-1.0, default: 1.0) |
headers |
Object | No | HTTP headers |
data |
Object | No | Request body (form data) |
json |
Object | No | JSON request body |
params |
Object | No | URL query parameters |
timeout |
Integer | No | Request timeout in seconds |
pre_request |
Array | No | Steps to run before this step |
pre_transforms |
Array | No | Data transformations before request |
post_transforms |
Array | No | Data transformations after request |
extract |
Object | No | Extract variables from response |
validate |
Object/Array | No | Response validation rules |
retry_on |
Object | No | Retry configuration |
skip_if |
Object | No | Conditional step execution |
| Field | Type | Required | Description |
|---|---|---|---|
type |
String | Yes | Plugin name |
input |
String | No | Input variable (template) |
output |
String | Yes | Output variable name |
config |
Object | No | Plugin-specific configuration |
| Field | Type | Description |
|---|---|---|
max_attempts |
Integer | Maximum retry attempts (default: 3) |
status_codes |
Array | HTTP status codes to retry on |
exceptions |
Array | Exception types to retry on |
steps:
- name: "Payment"
pre_request:
- "Get Fresh Token"
method: "POST"
endpoint: "/payments"pre_transforms:
- type: "random_string"
config:
length: 8
output: "password"
- type: "sha256"
input: "{{ password }}"
output: "hashed"
- type: "base64_encode"
input: "{{ hashed }}"
output: "final"# Setup
make install # Install dependencies
make install-dev # Install dev dependencies
# Validation
make validate-configs # Validate all YAML configs
# Testing
make test # Run unit tests
make coverage # Run tests with coverage report
# Running
make run # Start Locust web UI (validates first)
make run-headless # Run headless load test (validates first)
# Code Quality
make lint # Run linters
make format # Format code
# CI/CD
make ci # Run all CI checks
make all # Run everything| Practice | Recommendation | Reason |
|---|---|---|
| Config Validation | Always use make run |
Catches errors before runtime |
| Initialization | Use run_init_once: true |
Faster tests, less load on auth |
| User Management | Set init_list_var |
Proper multi-user setup |
| Load Distribution | Use round_robin for users |
Even distribution across accounts |
| User Behavior | Use random for actions |
Realistic traffic patterns |
| Step Weights | Match real user behavior | Accurate load simulation |
| Token Storage | Use store_data plugin |
Avoid re-authentication |
| Response Validation | Validate critical responses | Early error detection |
| Timeouts | Set appropriate timeouts | Prevent hanging requests |
| Retry Logic | Configure retries for transient errors | Handle network issues |
| Issue | Cause | Solution |
|---|---|---|
| Config validation fails | YAML syntax error | Check indentation, quotes, and structure |
| "Plugin not found" | Typo in plugin name | Check plugin name spelling |
| "Variable not found" | Missing variable | Ensure variable is defined or extracted |
| Authentication fails | Wrong credentials | Verify credentials in variables section |
| Timeout errors | Slow API or low timeout | Increase timeout value |
| Rate limiting | Too many requests | Reduce users or add delays |
| Token expired | Long test duration | Implement token refresh in pre_request |
# Run with verbose logging
LOG_LEVEL=DEBUG make run
# Validate specific config
python validate_config.py configs/your-config.yaml
# Run tests to verify setup
make test| Tip | Description | Impact |
|---|---|---|
Use run_init_once |
Initialize once, not per user | 10-100x faster startup |
| Optimize weights | Focus on critical paths | Better resource usage |
| Set realistic timeouts | Avoid hanging requests | Cleaner test results |
| Validate selectively | Only validate critical responses | Reduced overhead |
| Use connection pooling | Enabled by default | Better performance |
Check /configs/ directory for complete examples:
| File | Description | Features Demonstrated |
|---|---|---|
test.yaml |
Wave Money API | Multi-step auth, encryption, validation |
test-wave-pay.yaml |
50 users example | Multi-user setup, token storage, weights |
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Run tests:
make test - Run linters:
make lint - Format code:
make format - Submit a pull request
# Install development dependencies
make install-dev
# Install pre-commit hooks
make install-hooks
# Run all checks
make ci# Run unit tests
make test
# Run with coverage
make coverage
# Run specific test file
pytest tests/test_config_validator.py -v