| name | load-testing |
| description | This skill should be used when the user asks to "load test an API", "stress test the server", "benchmark this endpoint", "run performance tests", "create k6 script", "set up artillery test", "measure API throughput", "test under load", "find breaking point", "simulate concurrent users", or mentions load testing, stress testing, performance testing, benchmarking, throughput testing, k6, artillery, or JMeter. |
| license | MIT |
| metadata | {"author":"1mangesh1","version":"1.0.0","tags":["load-testing","performance","k6","artillery","benchmarking","stress-testing","throughput"]} |
Load Testing
Comprehensive guide for load testing, stress testing, and performance benchmarking of APIs, web services, and distributed systems.
Load Testing Fundamentals
Types of Performance Tests
| Test Type | Purpose | Pattern | Duration |
|---|
| Load Test | Validate expected concurrent users | Gradual ramp to target load | 15-60 min |
| Stress Test | Find breaking point | Ramp beyond expected capacity | 30-60 min |
| Spike Test | Handle sudden traffic surges | Instant jump to peak load | 5-15 min |
| Soak Test | Detect memory leaks, resource exhaustion | Steady moderate load | 4-24 hours |
| Breakpoint Test | Determine max capacity | Incremental ramp until failure | Until failure |
Key Metrics
- Throughput: Requests per second (RPS) the system handles
- Latency Percentiles: p50, p90, p95, p99 response times
- Error Rate: Percentage of failed requests (target < 1%)
- Concurrent Users: Number of simultaneous active connections
- Apdex Score: Application Performance Index (0 to 1)
Test Phases
Load
^
| ___________
| / \ Steady State
| / \ (measure here)
| / \
| / \
| / \
|/ \
+-------------------------> Time
Ramp-Up Hold Ramp-Down
k6 (Grafana k6) - Modern Load Testing
k6 is the recommended tool for most load testing scenarios. It uses JavaScript for test scripts and is built for developer productivity.
Installation
brew install k6
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D68
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] 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
docker run --rm -i grafana/k6 run - <script.js
choco install k6
Basic k6 Load Test
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
http_reqs: ['rate>100'],
},
};
export default function () {
const res = http.get('https://api.example.com/health');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': () => r.. < ,
: .(r.). === ,
});
();
}
k6 run load-test.js
k6 run -e BASE_URL=https://staging.example.com load-test.js
k6 run --vus 100 --duration 30s load-test.js
k6 Advanced Scenarios
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const loginDuration = new Trend('login_duration');
export const options = {
scenarios: {
constant_request_rate: {
executor: 'constant-arrival-rate',
rate: 100,
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
maxVUs: 200,
},
ramping_request_rate: {
executor: 'ramping-arrival-rate',
startRate: 10,
timeUnit: '1s',
stages: [
{ target: , : },
{ : , : },
{ : , : },
],
: ,
: ,
: ,
},
: {
: ,
: ,
: [
{ : , : },
{ : , : },
{ : , : },
],
: ,
},
},
: {
: [],
: [],
: [],
},
};
() {
(, () {
res = http.();
(res, { : r. === });
errorRate.(res. !== );
});
(, () {
loginStart = .();
loginRes = http.(, .({
: ,
: ,
}), {
: { : },
});
loginDuration.(.() - loginStart);
(loginRes, {
: r. === ,
: .(r.). !== ,
});
(loginRes. === ) {
token = .(loginRes.).;
profileRes = http.(, {
: { : },
});
(profileRes, { : r. === });
}
});
(.() * + );
}
k6 File Upload and Multipart Testing
import http from 'k6/http';
import { check } from 'k6';
const binFile = open('/path/to/file.png', 'b');
export default function () {
const res = http.post('https://api.example.com/upload', {
file: http.file(binFile, 'test.png', 'image/png'),
description: 'Load test upload',
});
check(res, {
'upload successful': (r) => r.status === 201,
'response time ok': (r) => r.timings.duration < 5000,
});
}
k6 WebSocket Testing
import ws from 'k6/ws';
import { check } from 'k6';
export const options = {
vus: 50,
duration: '5m',
};
export default function () {
const url = 'wss://api.example.com/ws';
const params = { headers: { Authorization: 'Bearer token123' } };
const res = ws.connect(url, params, function (socket) {
socket.on('open', () => {
console.log('WebSocket connected');
socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
});
socket.on('message', (msg) => {
const data = JSON.parse(msg);
check(data, {
'message has type': (d) => d.type !== ,
: d. !== ,
});
});
socket.(, .());
socket.(, .(, e.()));
socket.( () {
socket.();
}, );
});
(res, { : r && r. === });
}
k6 gRPC Testing
import grpc from 'k6/net/grpc';
import { check, sleep } from 'k6';
const client = new grpc.Client();
client.load(['definitions'], 'service.proto');
export const options = {
vus: 20,
duration: '3m',
thresholds: {
grpc_req_duration: ['p(95)<300'],
},
};
export default function () {
client.connect('grpc.example.com:443', { plaintext: false });
const response = client.invoke('api.v1.UserService/GetUser', {
user_id: '12345',
});
check(response, {
'status is OK': (r) => r && r.status === grpc.StatusOK,
'has user data': (r) => r && r.message.name !== '',
});
client.close();
sleep(1);
}
k6 Output and Reporting
k6 run --out json=results.json load-test.js
k6 run --out csv=results.csv load-test.js
k6 run --out influxdb=http://localhost:8086/k6 load-test.js
k6 run --out experimental-prometheus-rw load-test.js
k6 run --out json=results.json --out influxdb=http://localhost:8086/k6 load-test.js
Artillery - YAML-Based Load Testing
Artillery is great for teams who prefer YAML configuration and need built-in protocol support for HTTP, WebSocket, Socket.IO, and more.
Installation
npm install -g artillery
npx artillery
Basic Artillery Config
config:
target: "https://api.example.com"
phases:
- duration: 120
arrivalRate: 10
name: "Warm up"
- duration: 300
arrivalRate: 50
name: "Sustained load"
- duration: 60
arrivalRate: 100
name: "Peak load"
defaults:
headers:
Content-Type: "application/json"
Authorization: "Bearer {{ $processEnvironment.API_TOKEN }}"
ensure:
thresholds:
- http.response_time.p95: 500
- http.response_time.p99: 1000
- http.codes.500: 0
scenarios:
- name:
Run Artillery Tests
artillery run artillery-config.yml
artillery run --target https://staging.example.com artillery-config.yml
artillery run --output report.json artillery-config.yml
artillery report report.json --output report.html
artillery quick --count 100 --num 10 https://api.example.com/health
Artillery with Custom JavaScript
config:
target: "https://api.example.com"
processor: "./custom-functions.js"
phases:
- duration: 300
arrivalRate: 20
scenarios:
- name: "Authenticated flow"
flow:
- function: "generateUser"
- post:
url: "/api/auth/login"
json:
email: "{{ email }}"
password: "{{ password }}"
capture:
- json: "$.token"
as: "authToken"
- get:
url: "/api/dashboard"
headers:
Authorization: "Bearer {{ authToken }}"
module.exports = {
generateUser: function (context, events, done) {
const id = Math.floor(Math.random() * 10000);
context.vars.email = `loadtest_user_${id}@example.com`;
context.vars.password = 'TestPassword123!';
return done();
},
};
JMeter Basics
Apache JMeter is a Java-based tool for complex test plans with a GUI for design and CLI for execution.
CLI Execution (Recommended for CI/CD)
jmeter -n -t test-plan.jmx -l results.jtl -e -o report/
jmeter -n -t test-plan.jmx \
-Jthreads=100 \
-Jrampup=60 \
-Jduration=300 \
-Jhost=api.example.com \
-l results.jtl
jmeter -g results.jtl -o html-report/
JMeter Test Plan Structure
Test Plan
+-- Thread Group (users=100, ramp-up=60s, loops=forever, duration=300s)
+-- HTTP Request Defaults (server, port, protocol)
+-- HTTP Header Manager (Content-Type, Authorization)
+-- CSV Data Set Config (test data file)
+-- HTTP Request: GET /api/health
+-- HTTP Request: POST /api/login
+-- Response Assertion (status code = 200)
+-- JSON Extractor (extract token)
+-- HTTP Request: GET /api/data (with token)
+-- Summary Report
+-- Aggregate Report
+-- View Results Tree (debug only)
Locust (Python-Based)
Locust is ideal for Python teams. Define user behavior in Python code.
Installation and Basic Test
pip install locust
from locust import HttpUser, task, between, tag
class APIUser(HttpUser):
wait_time = between(1, 5)
host = "https://api.example.com"
def on_start(self):
"""Called once per user on start."""
response = self.client.post("/api/auth/login", json={
"email": "testuser@example.com",
"password": "password123"
})
self.token = response.json().get("token", "")
@tag("read")
@task(3)
def get_products(self):
self.client.get("/api/products", headers={
"Authorization": f"Bearer {self.token}"
})
@tag("read")
@task(2)
def get_product_detail(self):
product_id = 42
self.client.get(, headers={
:
})
():
.client.post(, json={
: ,
:
}, headers={
:
})
locust -f locustfile.py
locust -f locustfile.py --headless -u 100 -r 10 --run-time 5m \
--host https://api.example.com --csv results
locust -f locustfile.py --tags read --headless -u 50 -r 5 --run-time 3m
locust -f locustfile.py --master
locust -f locustfile.py --worker --master-host=192.168.1.10
wrk and wrk2 - HTTP Benchmarking
wrk and wrk2 are lightweight, high-performance HTTP benchmarking tools for maximum throughput testing.
wrk
brew install wrk
wrk -t12 -c400 -d30s https://api.example.com/health
wrk -t8 -c200 -d60s -s post.lua https://api.example.com/api/data
wrk -t4 -c100 -d30s --latency https://api.example.com/health
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"
wrk.body = '{"name": "load-test", "value": 42}'
request = function()
local id = math.random(1, 10000)
local body = string.format('{"user_id": %d, "action": "test"}', id)
return wrk.format("POST", "/api/events", nil, body)
end
done = function(summary, latency, requests)
io.write("------------------------------\n")
io.write(string.format("Total requests: %d\n", summary.requests))
io.write(string.format("Total errors: %d\n", summary.errors.status))
io.write(string.format("Avg latency: %.2fms\n", latency.mean / 1000))
io.write(string.(, latency. / ))
wrk2 (Constant Throughput)
wrk2 -t8 -c100 -d120s -R1000 --latency https://api.example.com/health
ab (Apache Bench) - Quick Tests
ab is pre-installed on most systems and ideal for quick, simple benchmarks.
ab -n 10000 -c 100 https://api.example.com/health
ab -n 5000 -c 50 -p payload.json -T application/json https://api.example.com/api/data
ab -n 10000 -c 200 -k -H "Authorization: Bearer token123" https://api.example.com/api/users
ab -n 100 -c 10 https://api.example.com/health
Thresholds and SLOs
Defining Performance Budgets
thresholds:
response_time:
p50: 100ms
p90: 250ms
p95: 500ms
p99: 1000ms
max: 5000ms
throughput:
minimum_rps: 500
target_rps: 1000
error_rate:
threshold: 0.1%
5xx_threshold: 0%
availability:
target: 99.95%
apdex:
target: 0.9
t_value: 500ms
k6 Threshold Examples
export const options = {
thresholds: {
http_req_duration: [
'p(50)<100',
'p(90)<250',
'p(95)<500',
'p(99)<1000',
'max<5000',
],
http_req_failed: ['rate<0.001'],
http_reqs: ['rate>100'],
'http_req_duration{name:login}': ['p(95)<2000'],
'http_req_duration{name:search}': ['p(95)<300'],
'group_duration{group:::checkout flow}': ['p(95)<10000'],
},
};
CI/CD Integration
GitHub Actions
name: Load Test
on:
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1'
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 \
--recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D68
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] 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
- name: Run load tests
run: k6 run --out json=results.json tests/load/api-load-test.js
env:
BASE_URL: ${{ secrets.STAGING_URL }}
API_TOKEN: ${{ secrets.LOAD_TEST_TOKEN }}
-
GitLab CI
load_test:
stage: test
image: grafana/k6:latest
script:
- k6 run --out json=results.json tests/load/api-load-test.js
artifacts:
paths:
- results.json
when: always
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_PIPELINE_SOURCE == "schedule"'
Distributed Load Testing
k6 with Kubernetes Operator
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
name: api-load-test
spec:
parallelism: 4
script:
configMap:
name: k6-test-script
file: load-test.js
arguments: --out influxdb=http://influxdb:8086/k6
runner:
resources:
limits:
cpu: "1"
memory: "1Gi"
requests:
cpu: "500m"
memory: "512Mi"
Locust Distributed with Docker Compose
version: '3'
services:
master:
image: locustio/locust
ports:
- "8089:8089"
volumes:
- ./:/mnt/locust
command: -f /mnt/locust/locustfile.py --master -H https://api.example.com
worker:
image: locustio/locust
volumes:
- ./:/mnt/locust
command: -f /mnt/locust/locustfile.py --worker --master-host master
deploy:
replicas: 4
Interpreting Results
Reading k6 Output
/\ |------| /\
/\ / \ | oo |/ \
/ \/ \ | | \
/ \ |______| \
execution: local
script: load-test.js
output: -
scenarios: (100.00%) 1 scenario, 50 max VUs, 9m30s max duration
default: Up to 50 looping VUs for 9m0s
data_received..................: 45 MB 83 kB/s
data_sent......................: 3.2 MB 5.9 kB/s
http_req_blocked...............: avg=2.3ms p(90)=4.5ms p(95)=6.1ms
http_req_connecting............: avg=1.8ms p(90)=3.2ms p(95)=4.5ms
✓ http_req_duration..............: avg=125ms p(90)=210ms p(95)=350ms <-- KEY METRIC
http_req_failed................: 0.12% ✓ 15 ✗ 12485 <-- ERROR RATE
http_req_receiving.............: avg=0.8ms p(90)=1.5ms p(95)=2.1ms
http_req_sending...............: avg=0.3ms p(90)=0.5ms p(95)=0.7ms
http_req_tls_handshaking.......: avg=5.2ms p(90)=8.1ms p(95)=10.3ms
http_req_waiting...............: avg=124ms p(90)=208ms p(95)=348ms
http_reqs......................: 12500 23.1/s <-- THROUGHPUT
iteration_duration.............: avg=1.13s p(90)=1.22s p(95)=1.36s
iterations.....................: 12500 23.1/s
vus............................: 1 min=1 max=50
vus_max........................: 50 min=50 max=50
What to Look For
| Metric | Healthy | Warning | Critical |
|---|
| p95 latency | < 500ms | 500ms - 1s | > 1s |
| p99 latency | < 1s | 1s - 3s | > 3s |
| Error rate | < 0.1% | 0.1% - 1% | > 1% |
| Throughput variance | < 10% | 10% - 25% | > 25% |
| CPU utilization | < 70% | 70% - 85% | > 85% |
| Memory utilization | < 75% | 75% - 90% | > 90% |
Common Bottleneck Patterns
CPU Bottleneck
- Symptoms: High CPU, linear latency increase with load, throughput plateau
- Common causes: Inefficient algorithms, excessive serialization/deserialization, regex backtracking
- Diagnosis: Profile with
perf, py-spy, or async-profiler
Memory Bottleneck
- Symptoms: Growing memory usage over time, GC pauses, OOM kills during soak tests
- Common causes: Memory leaks, large in-memory caches, unbounded queues
- Diagnosis: Heap dumps, memory profilers, monitor RSS over soak tests
Database Connection Pool Exhaustion
- Symptoms: Latency spikes at specific concurrency, connection timeout errors
- Common causes: Slow queries holding connections, pool too small, missing connection release
- Diagnosis: Monitor active/idle connections, query duration distribution
Network Bottleneck
- Symptoms: High bandwidth utilization, TCP connection errors, retransmissions
- Common causes: Large payloads, missing compression, connection limits
- Diagnosis:
netstat, ss, network monitoring, check payload sizes
Thread/Worker Pool Exhaustion
- Symptoms: Request queuing, latency increases while CPU stays low
- Common causes: Blocking I/O in async code, insufficient worker count, thread contention
- Diagnosis: Thread dumps, worker pool metrics, request queue depth
Realistic Test Data Generation
k6 Data-Driven Testing
import { SharedArray } from 'k6/data';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
const users = new SharedArray('users', function () {
return papaparse.parse(open('./test-users.csv'), { header: true }).data;
});
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
const res = http.post(`${__ENV.BASE_URL}/api/login`, JSON.stringify({
email: user.email,
password: user.password,
}), {
headers: { 'Content-Type': 'application/json' },
});
}
Generating Test Data
python3 -c "
import csv, random, string
with open('test-users.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['email', 'password', 'name'])
for i in range(10000):
name = ''.join(random.choices(string.ascii_lowercase, k=8))
writer.writerow([f'{name}_{i}@loadtest.example.com', 'TestPass123!', name])
print('Generated 10000 test users')
"
Performance Testing Best Practices
Before Testing
- Define objectives: What are the SLOs? What load is expected?
- Baseline metrics: Measure current performance before changes
- Isolate the environment: Use a dedicated staging environment
- Prepare test data: Use realistic, diverse datasets
- Warm up caches: Run a warm-up phase before measuring
During Testing
- Monitor the system: CPU, memory, disk I/O, network, DB connections
- Watch for errors: Check application and server logs in real time
- Correlate metrics: Cross-reference load generator data with server metrics
- Test incrementally: Start small, increase load gradually
- Document everything: Record test parameters, environment, and results
After Testing
- Analyze percentiles: Focus on p95/p99, not averages
- Compare to baseline: Look for regressions, not just absolute values
- Identify bottlenecks: Use profiling data to find root causes
- Share results: Publish dashboards and reports for the team
- Automate regression detection: Integrate into CI/CD pipeline
Anti-Patterns to Avoid
- Testing from the same machine as the server
- Using only averages instead of percentiles
- Running tests on shared/noisy environments
- Ignoring think time (unrealistic constant hammering)
- Testing with a single endpoint only
- Not monitoring the system under test
- Running tests without a warm-up phase
- Using hardcoded test data instead of realistic distributions
References