Sufast/Documentation
Load Testing
Load Testing Guide
Learn how to properly load test your Sufast applications to ensure they can handle production traffic.
Important: Always run load tests in a controlled environment that mirrors your production setup. Never run load tests against production systems without proper planning.
Setting Up Load Tests
Tools and setup for effective load testing
Installing k6
k6 Installationbash
# Install k6 on macOS
brew install k6
# Install k6 on Ubuntu/Debian
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
# Install k6 on Windows (using Chocolatey)
choco install k6
# Or download binary from https://k6.io/docs/getting-started/installation/Basic Load Test Script
Basic Load Testjavascript
// basic-load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
// Test configuration
export let options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 users over 2 minutes
{ duration: '5m', target: 100 }, // Stay at 100 users for 5 minutes
{ duration: '2m', target: 200 }, // Ramp up to 200 users over 2 minutes
{ duration: '5m', target: 200 }, // Stay at 200 users for 5 minutes
{ duration: '2m', target: 0 }, // Ramp down to 0 users over 2 minutes
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests must complete below 500ms
http_req_failed: ['rate<0.1'], // Error rate must be below 10%
},
};
export default function () {
// Test your Sufast API
let response = http.get('http://localhost:8080/api/users');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'response has users': (r) => r.json().users !== undefined,
});
// Simulate user think time
sleep(1);
}Running the Test
Running Testsbash
# Run the load test
k6 run basic-load-test.js
# Run with custom options
k6 run --vus 50 --duration 30s basic-load-test.js
# Generate detailed HTML report
k6 run --out html=report.html basic-load-test.js
# Run with environment variables
k6 run -e BASE_URL=http://localhost:8080 basic-load-test.jsAdvanced Load Testing Scenarios
Complex scenarios for comprehensive testing
API Workflow Testing
API Workflow Testjavascript
// api-workflow-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
vus: 50,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<1000'],
http_req_failed: ['rate<0.05'],
},
};
export default function () {
const baseUrl = 'http://localhost:8080';
// 1. Login to get authentication token
let loginResponse = http.post(`${baseUrl}/auth/login`, JSON.stringify({
username: 'testuser',
password: 'testpass'
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginResponse, {
'login successful': (r) => r.status === 200,
'token received': (r) => r.json().token !== undefined,
});
if (loginResponse.status !== 200) {
return; // Skip rest of test if login fails
}
const token = loginResponse.json().token;
const authHeaders = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
};
// 2. Create a new resource
let createResponse = http.post(`${baseUrl}/api/items`, JSON.stringify({
name: `Test Item ${Math.random()}`,
description: 'Load test item'
}), { headers: authHeaders });
check(createResponse, {
'item created': (r) => r.status === 201,
'item has id': (r) => r.json().id !== undefined,
});
if (createResponse.status === 201) {
const itemId = createResponse.json().id;
// 3. Retrieve the created resource
let getResponse = http.get(`${baseUrl}/api/items/${itemId}`, {
headers: authHeaders
});
check(getResponse, {
'item retrieved': (r) => r.status === 200,
'correct item': (r) => r.json().id === itemId,
});
// 4. Update the resource
let updateResponse = http.put(`${baseUrl}/api/items/${itemId}`, JSON.stringify({
name: 'Updated Test Item',
description: 'Updated description'
}), { headers: authHeaders });
check(updateResponse, {
'item updated': (r) => r.status === 200,
});
// 5. Delete the resource
let deleteResponse = http.del(`${baseUrl}/api/items/${itemId}`, {
headers: authHeaders
});
check(deleteResponse, {
'item deleted': (r) => r.status === 204,
});
}
sleep(1);
}Stress Testing
Stress Testjavascript
// stress-test.js
import http from 'k6/http';
import { check } from 'k6';
export let options = {
stages: [
{ duration: '5m', target: 100 }, // Normal load
{ duration: '5m', target: 200 }, // Above normal
{ duration: '5m', target: 300 }, // High load
{ duration: '5m', target: 400 }, // Stress level
{ duration: '5m', target: 500 }, // Breaking point
{ duration: '10m', target: 0 }, // Recovery
],
thresholds: {
http_req_duration: ['p(95)<2000'], // More lenient for stress test
http_req_failed: ['rate<0.2'], // Allow higher error rate
},
};
export default function () {
let response = http.get('http://localhost:8080/api/heavy-computation');
check(response, {
'status is not 5xx': (r) => r.status < 500,
'response time acceptable': (r) => r.timings.duration < 5000,
});
}Spike Testing
Spike Testjavascript
// spike-test.js
import http from 'k6/http';
import { check } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 100 }, // Normal load
{ duration: '1m', target: 1000 }, // Sudden spike
{ duration: '2m', target: 100 }, // Back to normal
{ duration: '1m', target: 1000 }, // Another spike
{ duration: '2m', target: 100 }, // Back to normal
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<1000'],
http_req_failed: ['rate<0.15'],
},
};
export default function () {
let response = http.get('http://localhost:8080/api/users');
check(response, {
'status is 200': (r) => r.status === 200,
'response time under 1s': (r) => r.timings.duration < 1000,
});
}Interpreting Results
Understanding load test metrics and identifying bottlenecks
Key Metrics to Monitor
| Metric | Description | Good Target |
|---|---|---|
| http_req_duration | Response time percentiles | p95 < 500ms |
| http_req_failed | Error rate percentage | < 1% |
| http_reqs | Total requests per second | Depends on requirements |
| vus | Virtual users (concurrent) | Based on expected load |
| data_received | Bandwidth usage | Monitor for capacity |
Sample Test Output
k6 Test Resultstext
β status is 200
β response time < 500ms
β response has users
checks.........................: 100.00% β 15000 β 0
data_received..................: 45 MB 1.5 MB/s
data_sent......................: 1.2 MB 40 kB/s
http_req_blocked...............: avg=1.2ms min=0s med=1ms max=15ms p(90)=2ms p(95)=3ms
http_req_connecting............: avg=0.8ms min=0s med=0s max=12ms p(90)=1ms p(95)=2ms
http_req_duration..............: avg=45ms min=12ms med=42ms max=180ms p(90)=65ms p(95)=85ms
{ expected_response:true }...: avg=45ms min=12ms med=42ms max=180ms p(90)=65ms p(95)=85ms
http_req_failed................: 0.00% β 0 β 5000
http_req_receiving.............: avg=0.5ms min=0.1ms med=0.4ms max=5ms p(90)=0.8ms p(95)=1.2ms
http_req_sending...............: avg=0.2ms min=0.05ms med=0.15ms max=2ms p(90)=0.3ms p(95)=0.5ms
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=44.3ms min=11.8ms med=41.5ms max=178ms p(90)=64ms p(95)=84ms
http_reqs......................: 5000 166.666667/s
iteration_duration.............: avg=1.04s min=1.01s med=1.04s max=1.18s p(90)=1.06s p(95)=1.08s
iterations.....................: 5000 166.666667/s
vus............................: 100 min=100 max=100
vus_max........................: 100 min=100 max=100Monitoring Your Sufast App
Performance Monitoringpython
from sufast import App, Request
import time
import psutil
import asyncio
app = App()
# Add performance monitoring middleware
@app.middleware("http")
async def performance_monitor(request: Request, call_next):
start_time = time.time()
start_memory = psutil.Process().memory_info().rss / 1024 / 1024 # MB
response = await call_next(request)
end_time = time.time()
end_memory = psutil.Process().memory_info().rss / 1024 / 1024 # MB
# Log performance metrics
duration = (end_time - start_time) * 1000 # ms
memory_diff = end_memory - start_memory
print(f"Request: {request.method} {request.url.path}")
print(f"Duration: {duration:.2f}ms")
print(f"Memory change: {memory_diff:.2f}MB")
print(f"Status: {response.status_code}")
print("---")
# Add metrics to response headers
response.headers["X-Response-Time"] = f"{duration:.2f}ms"
response.headers["X-Memory-Usage"] = f"{end_memory:.2f}MB"
return response
@app.get("/metrics")
def get_metrics():
"""Endpoint for monitoring tools to scrape metrics"""
process = psutil.Process()
return {
"cpu_percent": psutil.cpu_percent(),
"memory_mb": process.memory_info().rss / 1024 / 1024,
"memory_percent": process.memory_percent(),
"open_files": len(process.open_files()),
"connections": len(process.connections()),
"threads": process.num_threads(),
}