A high-performance load-testing tool with a C engine and a Python scripting interface, designed in the spirit of Gatling and JMeter but driven by plain Python.
This README is the primary documentation. For internals and contribution workflow see the Contributor Guide; for known issues and their fixes see the Security & Correctness Audit; for the full user API see docs/API.md.
- C engine on the hot path — worker-thread pool, libcurl HTTP, connection pooling, and a microsecond-resolution latency histogram (p95/p99).
- Python scripting — author scenarios, assertions, and reporting in Python.
- Multi-protocol — HTTP/HTTPS, TCP, UDP, MQTT (real); real RFC 6455 WebSocket and real databases (PostgreSQL, MySQL/MariaDB, MongoDB) where the build finds libcurl's WebSocket API and the respective client libs — each degrades to simulation otherwise (see the capability matrix below).
- Session management — thread-safe per-user session storage, cookie handling, and response→variable correlation.
- Authentication flows — Basic, Bearer, API Key, Form, OAuth 2.0, Custom.
- Assertions — response assertions (status, body, JSON path, headers, timing) and aggregate performance assertions (throughput, error rate, …).
- Reporting — Console, JSON, and HTML reporters, composable via
MultiReporter. - Load patterns — constant, ramp-up, spike, and stress helpers.
- Data-driven testing — CSV sources with sequential/random/circular/unique
distribution and
${var}substitution.
LoadSpiker runs either the C engine (when the compiled extension is present) or a pure-Python fallback. What each protocol actually does today:
| Protocol | C engine | Python fallback | Notes |
|---|---|---|---|
| HTTP/HTTPS | ✅ real (libcurl) | ✅ real (requests) |
Full request queue + worker pool in C. |
| TCP | ✅ real sockets | ✅ real sockets | Connection pool; binary payloads with embedded NULs send in full. |
| UDP | ✅ real sockets | ✅ real sockets | Endpoint pool. |
| MQTT | ✅ real MQTT 3.1.1 over TCP | Hand-rolled CONNECT/PUBLISH/SUBSCRIBE packets. | |
| WebSocket | ✅ real RFC 6455 (libcurl WS) / |
Real frames via libcurl's WebSocket API when built with HAVE_CURL_WEBSOCKETS; simulated where libcurl lacks WS support. |
|
| Database | ✅ real PostgreSQL (libpq) / MySQL (libmysqlclient) / MongoDB (libmongoc), each with |
Real connect/query when built with HAVE_LIBPQ / HAVE_MYSQL / HAVE_MONGOC; falls back to simulation when a client lib is absent. MongoDB query string is a JSON command document. |
Where a protocol falls back to simulation it returns realistic-looking responses and timings so you can build and validate scenarios, but it does not talk to a real server. The build degrades gracefully:
setup.pyprobescurl-config/pg_configand definesHAVE_CURL_WEBSOCKETS/HAVE_LIBPQonly when those are present. Tracked in the Contributor Guide.
A static HTML site also ships in docs/site/ (open docs/site/index.html); its
protocol/architecture/API pages are reconciled with the capability matrix above.
When in doubt, this README and the Contributor Guide are authoritative.
# 1. System dependencies
sudo apt-get install build-essential libcurl4-openssl-dev python3-dev pkg-config # Debian/Ubuntu
# brew install curl pkg-config # macOS
# 2. Get the source
git clone <repository-url>
cd LoadSpiker
# 3a. Build the C extension for the interpreter that will run your tests
python3 setup.py build_ext --inplace # produces loadspiker/loadspiker_c.<abi>.so
# 3b. (or) use the Makefile helpers
make install-deps
make installWhich build command?
python3 setup.py build_ext --inplacecompiles the extension for your Python interpreter and places it whereimport loadspikerfinds it.make buildproducesobj/loadspiker.soagainst whateverpython3-configresolves to — handy for a compile check, but the in-place build is what Python imports. After editing a C header, re-run with--force(setuptools doesn't track header dependencies). If imports look stale, delete leftover*.sofiles in the repo root /loadspiker/.
# Quick URL test
python3 cli.py https://httpbin.org/get -u 10 -d 30
# Interactive mode
python3 cli.py -i
# Advanced load pattern
python3 cli.py https://api.example.com -p "ramp:1:50:60" --html report.htmlfrom loadspiker import Engine, Scenario
# Create engine
engine = Engine(max_connections=100, worker_threads=4)
# Create scenario
scenario = Scenario("My Test")
scenario.get("https://httpbin.org/get")
scenario.post("https://httpbin.org/post", body='{"test": "data"}')
# Run test
results = engine.run_scenario(scenario, users=10, duration=60)
print(f"RPS: {results['requests_per_second']:.2f}")from loadspiker import Engine
from loadspiker.assertions import (
status_is, response_time_under, body_contains, json_path,
header_exists, run_assertions
)
engine = Engine(max_connections=50, worker_threads=4)
# Execute request
response = engine.execute_request("https://api.example.com/users/123")
# Define assertions
assertions = [
status_is(200, "Expected successful response"),
response_time_under(1000, "Response should be under 1 second"),
json_path("user.id", 123, message="User ID should match"),
json_path("user.email", exists=True),
header_exists("content-type", "application/json"),
body_contains("user")
]
# Run assertions
success, failures = run_assertions(response, assertions)
if not success:
print("Assertion failures:", failures)LoadSpiker is three layers; the hot path lives in C while authoring stays in Python.
Layer 3 Python authoring loadspiker/*.py, cli.py
(Engine wrapper, Scenario builders, assertions,
reporters, sessions/auth, CSV data sources)
│ keyword-argument method calls
Layer 2 CPython C extension src/python_extension.c
(loadspiker.Engine type: marshal args → C structs,
release the GIL around I/O, build result dicts)
│ engine_* / protocol_* C calls
Layer 1 C engine core src/engine.c + src/protocols/*.c
(worker-thread pool, request queue, libcurl HTTP,
metrics histogram, per-protocol modules)
Threading. engine_create starts a pool of worker threads. A load test
(start_load_test) additionally spawns up to min(users, num_requests)
per-test workers that drain a shared request ring buffer and exit when it is
empty or a hard timeout (duration + 5s) elapses. Metrics are updated under a
dedicated metrics_mutex; the queue is guarded by queue_mutex + a condition
variable; shutdown/cancel flags are atomic. The build is verified race-free with
ThreadSanitizer (make tsan).
Metrics. Each request is recorded into a 1 ms-bucket latency histogram
(0–10 s + overflow). get_metrics() derives p95/p99 from the histogram and RPS
from wall-clock elapsed time.
For a full internals tour (data structures, lock ordering, the C↔Python boundary rules, and how to add a protocol) read the Contributor Guide.
- Scenario and config files execute arbitrary code.
python3 cli.py -s scenario.pyimports and runs a Python file; a JSON config drives requests. Treat scenario/config files like executables — only run ones you authored or trust. - Point load tests only at systems you are authorized to test. A load tool is, by definition, a traffic generator.
- Credentials passed to MQTT/HTTP auth are used to build requests and are not persisted by the engine (the MQTT module explicitly wipes the password after the CONNECT packet is sent).
See the Security & Correctness Audit for the full findings list and verification steps.
from loadspiker import Engine, RESTAPIScenario
from loadspiker.assertions import status_is, json_path, response_time_under
engine = Engine(max_connections=200)
scenario = RESTAPIScenario("https://api.example.com")
# Add requests with assertions
scenario.get_resource("users", assertions=[
status_is(200),
json_path("users", exists=True),
response_time_under(500)
])
scenario.create_resource("users", {"name": "Test User"}, assertions=[
status_is(201),
json_path("user.id", exists=True),
json_path("user.name", "Test User")
])
scenario.update_resource("users/1", {"name": "Updated User"}, assertions=[
status_is(200),
json_path("user.name", "Updated User")
])
scenario.delete_resource("users/1", assertions=[status_is(204)])
results = engine.run_scenario(scenario, users=25, duration=60)from loadspiker import Engine, WebsiteScenario
engine = Engine(max_connections=100)
scenario = WebsiteScenario("https://example.com")
scenario.browse_page("/")
scenario.browse_page("/products", think_time=2.0)
scenario.search("laptop")
scenario.login("user@example.com", "password")
results = engine.run_scenario(scenario, users=15, duration=120)from loadspiker import Engine
from loadspiker.scenarios import MQTTScenario
engine = Engine(max_connections=100, worker_threads=4)
# Create MQTT scenario for IoT device simulation
mqtt_scenario = MQTTScenario(
broker_host="test.mosquitto.org",
broker_port=1883,
client_id="loadspiker_iot_test",
name="IoT Sensor Load Test"
)
# Add various MQTT test patterns
mqtt_scenario.add_publish_test(
topic="sensors/temperature",
payload="25.3",
qos=1,
retain=True
)
mqtt_scenario.add_burst_publish_test(
topic="sensors/data",
message_count=100,
base_payload="sensor reading",
qos=0
)
mqtt_scenario.add_topic_pattern_test(
topic_pattern="devices/+/status",
payload="online",
topic_count=50,
qos=1
)
# Run MQTT load test
results = engine.run_scenario(mqtt_scenario, users=25, duration=120)
print(f"MQTT throughput: {results['requests_per_second']:.2f} messages/sec")
# Direct MQTT operations
engine = Engine()
# Connect and publish
response = engine.mqtt_connect("test.mosquitto.org", 1883, "test_client")
response = engine.mqtt_publish(
broker_host="test.mosquitto.org",
broker_port=1883,
client_id="test_client",
topic="loadtest/data",
payload="Hello MQTT!",
qos=1,
retain=False
)
# Subscribe with wildcards
response = engine.mqtt_subscribe(
broker_host="test.mosquitto.org",
broker_port=1883,
client_id="test_client",
topic="sensors/+/temperature", # Single-level wildcard
qos=0
)from loadspiker.utils import ramp_up, spike_test, stress_test
# Gradual ramp-up
for users, duration in ramp_up(1, 100, 300):
engine.run_scenario(scenario, users, duration)
# Spike testing
for users, duration in spike_test(20, 100, 60, 30):
engine.run_scenario(scenario, users, duration)
# Stress testing
for users, duration in stress_test(200, step_size=20):
engine.run_scenario(scenario, users, duration)from loadspiker import Engine
from loadspiker.session_manager import get_session_manager
from loadspiker.authentication import (
get_authentication_manager, create_basic_auth, create_bearer_auth,
create_api_key_auth, create_form_auth, create_oauth2_auth
)
engine = Engine()
session_manager = get_session_manager()
auth_manager = get_authentication_manager()
# Register different authentication methods
auth_manager.register_flow("basic", create_basic_auth("user", "pass"))
auth_manager.register_flow("api_key", create_api_key_auth("sk-123", "X-API-Key"))
auth_manager.register_flow("bearer", create_bearer_auth(token="jwt_token_xyz"))
auth_manager.register_flow("form", create_form_auth(
login_url="https://example.com/login",
username_field="email",
password_field="password",
success_indicator="Welcome"
))
# Authenticate different users with different methods
for user_id, auth_method in [("user1", "basic"), ("user2", "api_key"), ("user3", "bearer")]:
result = auth_manager.authenticate(auth_method, engine, user_id)
print(f"{user_id} authenticated: {result['success']}")
# Session management with request correlation
login_response = engine.execute_request(
url="https://api.example.com/login",
method="POST",
headers={"Content-Type": "application/json"},
body='{"username": "testuser", "password": "testpass"}'
)
# Auto-handle cookies and extract values for correlation
session_manager.auto_handle_cookies("user1", login_response)
extract_rules = [
{"type": "json_path", "path": "access_token", "variable": "token"},
{"type": "json_path", "path": "user.id", "variable": "user_id"},
{"type": "header", "name": "X-Session-ID", "variable": "session_id"},
{"type": "cookie", "name": "csrf_token", "variable": "csrf"}
]
session_manager.process_response("user1", login_response, extract_rules)
# Use session data in subsequent requests
headers = session_manager.prepare_request_headers("user1", {
"Content-Type": "application/json"
})
protected_response = engine.execute_request(
url="https://api.example.com/protected",
headers=headers # Automatically includes session cookies and auth tokens
)
# Access extracted session variables
session = session_manager.get_session("user1")
user_id = session.get("user_id")
csrf_token = session.get("csrf")
print(f"User {user_id} session established with CSRF: {csrf_token}")
# Multi-user session isolation
for user in ["alice", "bob", "charlie"]:
auth_manager.authenticate("basic", engine, user_id=user)
session = session_manager.get_session(user)
session.set("role", f"{user}_role")
# Each user maintains separate session state
print("Session isolation test:")
for user in ["alice", "bob", "charlie"]:
session = session_manager.get_session(user)
print(f"{user}: role={session.get('role')}, cookies={session.get_all_cookies()}")from loadspiker import Engine, Scenario
from loadspiker.data_sources import load_csv_data, get_user_data
# Load CSV data for parameterized testing
load_csv_data("users.csv", strategy="sequential")
engine = Engine(max_connections=100)
scenario = Scenario("User Login Test")
# CSV file format: username,password,email,user_id,subscription_type
# Data is automatically distributed among virtual users
def user_login(user_id):
user_data = get_user_data(user_id)
return scenario.post("/api/login", body=f'''{
"username": "{user_data['username']}",
"password": "{user_data['password']}"
}''')
# Or use built-in scenario CSV support with variable substitution
scenario.load_data_file("users.csv", strategy="sequential")
scenario.post("/api/login", body='{"username": "${data.username}", "password": "${data.password}"}')
# Multiple CSV files for complex scenarios
scenario.load_data_file("users.csv", name="users", strategy="sequential")
scenario.load_data_file("products.csv", name="products", strategy="random")
scenario.post("/api/order", body='{"user_id": "${users.user_id}", "product_id": "${products.product_id}"}')
results = engine.run_scenario(scenario, users=20, duration=60)from loadspiker.reporters import HTMLReporter, JSONReporter, MultiReporter
# Multiple report formats
reporter = MultiReporter([
ConsoleReporter(show_progress=True),
HTMLReporter("report.html"),
JSONReporter("results.json")
])
reporter.start_reporting()
results = engine.run_scenario(scenario, users=50, duration=120)
reporter.report_metrics(results)
reporter.end_reporting(){
"type": "rest_api",
"name": "API Load Test",
"base_url": "https://api.example.com",
"requests": [
{
"url": "https://api.example.com/health",
"method": "GET"
},
{
"url": "https://api.example.com/users",
"method": "POST",
"body": "{\"name\": \"Test User\"}",
"headers": {"Content-Type": "application/json"}
}
],
"variables": {
"api_key": "your-api-key"
}
}python3 cli.py -c config.json -u 20 -d 60# scenario.py
from loadspiker import Scenario
from loadspiker.scenarios import HTTPRequest
scenario = Scenario("Custom Test")
scenario.add_request(HTTPRequest("https://api.example.com/endpoint1", "GET"))
scenario.add_request(HTTPRequest("https://api.example.com/endpoint2", "POST", body='{"data": "test"}'))python3 cli.py -s scenario.py -u 30 -d 90LoadSpiker keeps the request/metrics path in C with fixed-size buffers (no
per-request allocation) and a lock-light worker pool. Actual throughput depends
heavily on the target, the network, and your max_connections/worker_threads
settings, so benchmark against your own workload rather than relying on a
headline number. Measure with:
make benchmark # runs benchmarks/benchmark_engine.pyLoadSpiker [OPTIONS] [URL]
Positional Arguments:
URL Target URL for simple tests
Load Parameters:
-u, --users Number of concurrent users (default: 10)
-d, --duration Test duration in seconds (default: 60)
-r, --ramp-up Ramp-up duration in seconds (default: 0)
-p, --pattern Load pattern: "constant:100:60", "ramp:1:100:60"
Engine Configuration:
--max-connections Maximum connections (default: 1000)
--threads Worker threads (default: 10)
Request Options:
-m, --method HTTP method (default: GET)
-H, --header HTTP header (repeatable)
-b, --body Request body
-t, --timeout Request timeout in ms (default: 30000)
Input Sources:
-s, --scenario Python scenario file
-c, --config JSON configuration file
-i, --interactive Interactive mode
Output Options:
--json Save results to JSON file
--html Save results to HTML file
-q, --quiet Suppress output
--no-progress Disable progress reporting# Install dependencies
make install-deps
# Build C extension
make build
# Install LoadSpiker
make install
# Run tests
make test
# Run examples
make example
make quick-test
# Clean build
make clean
# Development setup
make dev-setup
# Create package
make packageSymptoms: Program crashes with "segmentation fault" when making HTTP requests.
Causes:
- Memory corruption in C extension
- Uninitialized buffers
- Buffer overflow in response handling
Solutions:
# 1. Build the AddressSanitizer debug version
make clean
make debug # produces obj/loadspiker_debug.so
# 2. Run the suite under ASan (rebuilds + runs pytest)
make test-asan
# 3. Or run your own script with leak detection (Linux)
ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 python3 your_test.py
# 4. If AddressSanitizer is unavailable, try Valgrind (Linux)
valgrind --tool=memcheck --leak-check=full python3 your_test.pySymptoms: ImportError: No module named 'loadspiker' when running Python scripts.
Solutions:
# 1. Set PYTHONPATH manually
export PYTHONPATH=/path/to/LoadSpiker:$PYTHONPATH
# 2. Use the activation script
source activate_env.sh
# 3. Verify the compiled extension exists for your interpreter
ls -la loadspiker/loadspiker_c*.so
# If missing, build it: python3 setup.py build_ext --inplace --force
# 4. Install in development mode (requires virtual environment)
python3 -m pip install -e .Symptoms: Compilation errors during make build.
Common Issues:
# Missing libcurl headers
sudo apt-get install libcurl4-openssl-dev # Ubuntu/Debian
sudo yum install libcurl-devel # CentOS/RHEL
brew install curl # macOS
# Missing Python headers
sudo apt-get install python3-dev # Ubuntu/Debian
sudo yum install python3-devel # CentOS/RHEL
# Missing pkg-config
sudo apt-get install pkg-config # Ubuntu/Debian
sudo yum install pkgconfig # CentOS/RHEL
brew install pkg-config # macOSSymptoms: Excessive memory consumption during load tests.
Solutions:
- Reduce
max_connectionsparameter - Decrease
worker_threadscount - Use smaller buffer sizes for response bodies
- Implement request throttling
Symptoms: Lower than expected requests per second.
Debugging Steps:
# 1. Check system limits
ulimit -n # File descriptor limit
cat /proc/sys/net/core/somaxconn # Linux connection limit
# 2. Monitor system resources
top -p $(pgrep python3)
iostat 1
netstat -an | grep ESTABLISHED | wc -l
# 3. Profile the application
python3 -m cProfile -o profile.out your_test.py
python3 -c "import pstats; p=pstats.Stats('profile.out'); p.sort_stats('cumulative').print_stats(20)"Symptoms: Inconsistent results, crashes in multi-threaded scenarios.
Solutions:
- Verify thread-safe usage of the engine
- Check for shared state between threads
- Use separate engine instances for each thread if needed
Enable debug mode for detailed logging and error information:
import logging
logging.basicConfig(level=logging.DEBUG)
from loadspiker import Engine
engine = Engine(max_connections=10, worker_threads=2) # Use smaller values for debugging- Check this troubleshooting section
- Review the Contributing Guide for development setup
- Search existing issues on GitHub
- Create a new issue with:
- Complete error message
- System information (OS, Python version)
- Minimal reproduction case
- Build configuration used
- System: Linux, macOS, or Windows
- Python: 3.7+
- Dependencies: libcurl, pkg-config
- Build Tools: GCC or Clang, Make
sudo apt-get update
sudo apt-get install build-essential libcurl4-openssl-dev python3-dev pkg-configsudo yum groupinstall "Development Tools"
sudo yum install libcurl-devel python3-devel pkgconfig# Install Xcode Command Line Tools
xcode-select --install
# Install dependencies via Homebrew
brew install curl pkg-config- Install Visual Studio Build Tools
- Install curl development libraries
- Install Python development headers
We welcome contributions! Start with the Contributor Guide — a deep tour of the codebase covering architecture, the threading model, the C↔Python boundary, build/debug workflow, and a PR checklist. The short CONTRIBUTING.md is a quickstart. Before changing the C engine, also skim the Security & Correctness Audit.
Key topics:
- Setting up the development environment (and the two build paths)
- The three-layer architecture and threading model
- Reference-counting / GIL rules at the C↔Python boundary
- Testing (
make test), ASan (make test-asan), and TSan (make tsan) - Submitting pull requests
Quick start:
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Make your changes following our style guide
- Add tests and ensure they pass:
make test - Submit a pull request with a clear description
MIT License - see LICENSE file for details.
| Feature | LoadSpiker | Gatling | JMeter | Artillery |
|---|---|---|---|---|
| Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| Ease of Use | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| Scripting | Python | Scala | GUI/XML | JavaScript |
| Resource Usage | Very Low | Low | High | Medium |
| Real-time Metrics | ✅ | ✅ | ✅ | ✅ |
| HTML Reports | ✅ | ✅ | ✅ | ✅ |
| CI/CD Integration | ✅ | ✅ | Limited | ✅ |
LoadSpiker combines the performance of Gatling with the simplicity of Python scripting, making it ideal for both developers and QA engineers.
