TL;DR
- Load testing: Measuring system performance under expected traffic
- Goal: Verify application handles normal and peak user loads
- Key metrics: Response time (p95), throughput, error rate
- Popular tools: k6 (modern), JMeter (GUI), Gatling (Scala)
- Best practice: Test in production-like environments with realistic data
- When to run: Before releases, after major changes, regularly in CI/CD
Reading time: 12 minutes
Load testing ensures your application can handle real-world traffic. Without it, you discover performance problems from angry users — or worse, during a critical business moment.
What is Load Testing?
Load testing measures how a system performs under expected user traffic. It simulates many concurrent users making requests to verify the application handles the load without slowing down or failing.
Normal traffic: 100 users/minute → Response time: 200ms ✓
Peak traffic: 500 users/minute → Response time: 350ms ✓
Over capacity: 1000 users/minute → Response time: 5000ms ✗
Load testing finds the answer to: “Can our system handle the expected traffic?”
Why Load Testing Matters
1. Performance Problems Are Expensive
Slow applications cost money:
| Response Time | Impact |
|---|---|
| Under 1 second | Users stay engaged |
| 1-3 seconds | 40% abandon |
| 3+ seconds | 70%+ abandon |
| 10+ seconds | Users rarely return |
Every 100ms of latency can reduce conversion rates by 1%.
2. Production Failures Are Worse
Finding problems in production means:
- Lost revenue during outages
- Damaged reputation
- Emergency fixes under pressure
- Potential data loss
Load testing finds these issues before users do.
3. Traffic Is Unpredictable
Real traffic patterns vary:
Monday 9am: Spike from work hours
Friday 5pm: Drop as users leave work
Black Friday: 10x normal traffic
Viral moment: 100x normal traffic (if you're lucky)
Load testing prepares you for peaks.
Types of Performance Testing
Load Testing
Tests expected traffic levels:
// k6 load test - expected traffic
export const options = {
stages: [
{ duration: '5m', target: 100 }, // Ramp up
{ duration: '10m', target: 100 }, // Steady state
{ duration: '5m', target: 0 }, // Ramp down
],
};
Goal: Verify normal operation.
Stress Testing
Pushes beyond expected limits:
// k6 stress test - find breaking point
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 200 },
{ duration: '5m', target: 400 },
{ duration: '5m', target: 800 }, // Beyond normal
{ duration: '2m', target: 0 },
],
};
Goal: Find maximum capacity and breaking points.
Spike Testing
Tests sudden traffic surges:
// k6 spike test - sudden surge
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '30s', target: 500 }, // Sudden spike
{ duration: '2m', target: 500 },
{ duration: '30s', target: 50 }, // Drop back
],
};
Goal: Verify system handles sudden changes.
Soak Testing
Tests performance over extended periods:
// k6 soak test - extended duration
export const options = {
stages: [
{ duration: '5m', target: 100 },
{ duration: '8h', target: 100 }, // 8 hours
{ duration: '5m', target: 0 },
],
};
Goal: Find memory leaks and degradation over time.
Key Metrics to Track
Response Time
How long requests take to complete:
p50 (median): 200ms - Half of requests faster
p95: 500ms - 95% of requests faster
p99: 1200ms - 99% of requests faster
Max: 5000ms - Slowest request
Focus on p95/p99 — these show worst-case user experience.
Throughput
Requests processed per unit time:
Requests/second: 150 req/s
Transactions/second: 30 tx/s
Data transfer: 15 MB/s
Higher throughput means more capacity.
Error Rate
Percentage of failed requests:
Successful requests: 9,850
Failed requests: 150
Error rate: 1.5% ← Too high for production
Target: Under 1% error rate, ideally under 0.1%.
Resource Usage
Server-side metrics:
CPU: 75% average, 95% peak
Memory: 4GB / 8GB (50%)
Disk I/O: 200 IOPS
Network: 500 Mbps
Watch for bottlenecks approaching limits.
Writing Your First Load Test
k6 (JavaScript)
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50, // 50 virtual users
duration: '5m', // Run for 5 minutes
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'], // Less than 1% errors
},
};
export default function () {
// Test homepage
const homeResponse = http.get('https://example.com/');
check(homeResponse, {
'homepage status is 200': (r) => r.status === 200,
'homepage loads fast': (r) => r.timings.duration < 500,
});
// Simulate user think time
sleep(1);
// Test API endpoint
const apiResponse = http.get('https://example.com/api/products');
check(apiResponse, {
'api status is 200': (r) => r.status === 200,
'api returns data': (r) => JSON.parse(r.body).length > 0,
});
sleep(2);
}
Run with: k6 run load-test.js
JMeter
JMeter uses XML configuration, typically created via GUI:
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan>
<hashTree>
<ThreadGroup>
<stringProp name="ThreadGroup.num_threads">50</stringProp>
<stringProp name="ThreadGroup.ramp_time">30</stringProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
<HTTPSamplerProxy>
<stringProp name="HTTPSampler.domain">example.com</stringProp>
<stringProp name="HTTPSampler.path">/api/products</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
</ThreadGroup>
</hashTree>
</jmeterTestPlan>
Run with: jmeter -n -t test.jmx -l results.jtl
Locust (Python)
# locustfile.py
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 3) # Think time
@task(3) # Higher weight
def view_homepage(self):
self.client.get("/")
@task(2)
def view_products(self):
self.client.get("/api/products")
@task(1)
def view_product_detail(self):
self.client.get("/api/products/1")
Run with: locust -f locustfile.py --host=https://example.com
Load Testing Best Practices
1. Test in Production-Like Environments
Environment matters:
❌ Testing on localhost with 1GB database
✓ Testing on staging with production-size data
Localhost results don't predict production performance
Match:
- Server specifications
- Database size
- Network configuration
- Third-party integrations
2. Use Realistic Test Data
// Bad: Always same request
http.get('/api/users/1');
// Good: Varied, realistic requests
const userId = Math.floor(Math.random() * 10000) + 1;
http.get(`/api/users/${userId}`);
Vary parameters to hit different code paths and cache behaviors.
3. Simulate Real User Behavior
Users don’t click as fast as scripts:
export default function () {
http.get('/');
sleep(2); // Think time: reading homepage
http.get('/products');
sleep(3); // Think time: browsing products
http.post('/cart', { productId: 123 });
sleep(1); // Quick action
}
Include realistic delays between actions.
4. Define Clear Pass/Fail Criteria
Set thresholds before testing:
export const options = {
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
http_reqs: ['rate>100'], // Minimum throughput
},
};
Tests should pass or fail automatically.
5. Test Regularly
Load testing shouldn’t be a one-time event:
# GitHub Actions - weekly load test
on:
schedule:
- cron: '0 2 * * 0' # Every Sunday at 2am
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: k6 run tests/load-test.js
Catch regressions before they reach production.
Load Testing Tools Comparison
| Tool | Language | Best For | Learning Curve |
|---|---|---|---|
| k6 | JavaScript | Developers, CI/CD | Low |
| JMeter | Java/XML | QA teams, GUI users | Medium |
| Gatling | Scala | High performance | Medium |
| Locust | Python | Python teams | Low |
| Artillery | JavaScript | Serverless apps | Low |
Choosing a Tool
Choose k6 if:
- Developers write tests
- Need CI/CD integration
- Prefer code over GUI
Choose JMeter if:
- QA team writes tests
- Need visual test design
- Extensive plugin ecosystem needed
Choose Gatling if:
- Need high performance
- Scala/JVM environment
- Detailed HTML reports
Interpreting Results
Healthy Results
✓ http_req_duration..........: avg=180ms p95=320ms p99=450ms
✓ http_req_failed............: 0.02%
✓ http_reqs..................: 15000 (250/s)
✓ vus........................: 50
Low response times, minimal errors, consistent throughput.
Problem Indicators
✗ http_req_duration..........: avg=2500ms p95=8000ms p99=15000ms
✗ http_req_failed............: 15%
http_reqs..................: 3000 (50/s)
vus........................: 50
Red flags:
- High response times (especially p95/p99)
- Error rate above 1%
- Throughput drops under load
- High resource usage
Finding Bottlenecks
When tests fail, investigate:
- Application code — Slow queries, inefficient algorithms
- Database — Query optimization, indexing, connection pooling
- Infrastructure — CPU/memory limits, network bandwidth
- Third parties — Slow external APIs, rate limits
// Add detailed logging to identify bottlenecks
const response = http.get('/api/users');
console.log(`DNS: ${response.timings.blocked}ms`);
console.log(`Connect: ${response.timings.connecting}ms`);
console.log(`TLS: ${response.timings.tls_handshaking}ms`);
console.log(`Waiting: ${response.timings.waiting}ms`);
console.log(`Receiving: ${response.timings.receiving}ms`);
FAQ
What is load testing?
Load testing measures how a system performs under expected user traffic. It simulates many concurrent users making requests simultaneously to verify the application handles normal and peak loads without performance degradation. Unlike functional testing that checks if features work, load testing checks if features work under realistic traffic conditions. The goal is to answer: “Can our system handle the expected number of users?”
What’s the difference between load testing and stress testing?
Load testing simulates expected traffic levels to verify normal operation. Stress testing intentionally exceeds expected limits to find breaking points. Think of load testing as “can we handle normal traffic?” and stress testing as “when do we break?” Load testing uses realistic user numbers; stress testing pushes until the system fails. Both are valuable — load testing for validation, stress testing for capacity planning.
What metrics should I track in load testing?
Track these key metrics: Response time percentiles (p50, p95, p99) show how users experience performance — p95 means 95% of requests are faster than this value. Throughput (requests per second) shows capacity. Error rate (percentage of failed requests) should stay under 1%. Resource usage (CPU, memory, disk I/O) reveals bottlenecks. Focus on p95 response time as your primary metric — it represents real user experience better than averages.
What tools are used for load testing?
Popular load testing tools include k6 (JavaScript-based, modern, developer-friendly), JMeter (Java, GUI-based, extensive plugins), Gatling (Scala, high performance), and Locust (Python-based, easy to learn). k6 is ideal for developers who want tests as code in CI/CD. JMeter suits QA teams preferring visual test design. Choose based on your team’s skills and integration needs.
See Also
- k6 vs JMeter - Load testing tool comparison
- JMeter vs Gatling - Performance testing tools
- JMeter Tutorial - Getting started with JMeter
- Performance Testing Guide - Best practices
