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 TimeImpact
Under 1 secondUsers stay engaged
1-3 seconds40% abandon
3+ seconds70%+ abandon
10+ secondsUsers 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

ToolLanguageBest ForLearning Curve
k6JavaScriptDevelopers, CI/CDLow
JMeterJava/XMLQA teams, GUI usersMedium
GatlingScalaHigh performanceMedium
LocustPythonPython teamsLow
ArtilleryJavaScriptServerless appsLow

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:

  1. Application code — Slow queries, inefficient algorithms
  2. Database — Query optimization, indexing, connection pooling
  3. Infrastructure — CPU/memory limits, network bandwidth
  4. 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