Load testing is how you find out your API can handle 10 concurrent users but fails at 50. Without it, you discover your scaling limits when real users hit them — at the worst possible moment.
In 2025, you have better options than setting up JMeter XML configs for three hours. Here's what actually works.
Why Load Testing Still Gets Skipped
Most teams skip load testing because:
- Tool complexity: JMeter requires XML configuration and a Java environment
- Time investment: Setting up a realistic test feels like a project in itself
- Interpretation difficulty: What do the numbers actually mean?
- CI integration friction: Running load tests in a pipeline adds complexity
Modern tools have addressed points 1-3 significantly. Load testing is more accessible than it's ever been.
What You're Actually Testing
Before choosing a tool, know what you need to measure:
- Throughput: Requests per second your system can handle
- Latency: Response time at different load levels (p50, p95, p99)
- Concurrency: How many simultaneous users before degradation
- Error rate: At what point does your system start failing requests
- Resource saturation: CPU, memory, database connections at load
Different tools expose these differently. The numbers that matter for you depend on your SLAs and user expectations.
k6 — The Developer-Friendly Standard
k6 is the tool that modernized load testing for developers. Scripts are JavaScript, the CLI is fast, and CI integration is straightforward.
Installation
# macOS
brew install k6
# Linux
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
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
Basic Script
// load-test.js
import http from 'k6/http'
import { check, sleep } from 'k6'
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up to 20 users
{ duration: '1m', target: 20 }, // Stay at 20 users
{ duration: '10s', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // Error rate under 1%
},
}
export default function () {
const res = http.get('https://api.example.com/users')
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
})
sleep(1)
}
# Run it
k6 run load-test.js
# Output summary
Testing an API with Auth
import http from 'k6/http'
import { check } from 'k6'
const BASE_URL = __ENV.BASE_URL || 'https://api.example.com'
const API_KEY = __ENV.API_KEY
export const options = {
vus: 50, // Virtual users
duration: '2m',
}
export default function () {
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
}
// Test a POST endpoint
const payload = JSON.stringify({
name: 'test user',
email: `user-${__VU}-${__ITER}@example.com`,
})
const createRes = http.post(`${BASE_URL}/users`, payload, { headers })
check(createRes, { 'user created': (r) => r.status === 201 })
// Test a GET endpoint
const userId = createRes.json('id')
const getRes = http.get(`${BASE_URL}/users/${userId}`, { headers })
check(getRes, { 'user fetched': (r) => r.status === 200 })
}
CI Integration (GitHub Actions)
# .github/workflows/load-test.yml
name: Load Test
on:
push:
branches: [main]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6 load test
uses: grafana/k6-action@v0.3.1
with:
filename: tests/load/api-test.js
env:
BASE_URL: https://staging.example.com
API_KEY: ${{ secrets.STAGING_API_KEY }}
K6_CLOUD_TOKEN: ${{ secrets.K6_CLOUD_TOKEN }} # Optional: for Grafana Cloud k6
k6 Cloud (Grafana Cloud k6)
For large-scale tests that would overwhelm a CI runner, k6 offers a cloud execution environment. Tests run on distributed infrastructure and results appear in a Grafana dashboard.
# Run 100,000 VU test in the cloud
k6 cloud load-test.js
Best For
- Developer-owned load testing
- API testing with complex auth flows
- CI integration
- Teams that want JavaScript scripting
Pricing: CLI is open source/free. Cloud execution: free tier available, paid starts at ~$9/month.
Apache JMeter — The Enterprise Standard
JMeter has been around since 1998. It's Java-based, uses XML for test configuration, and has a GUI. It's the tool most enterprises already have configured — often in legacy CI systems.
When JMeter Makes Sense
- Your organization already uses it
- You need JDBC (database) load testing
- You need LDAP, SMTP, or FTP testing
- You need the GUI for non-technical team members
When JMeter Doesn't Make Sense
- Starting fresh — the XML configuration and Java environment requirement is unnecessary complexity
- You need clean CI integration — JMeter's command-line output is harder to parse
- Your team wants to write tests in code
Basic JMeter CLI Usage
# Run a JMeter test from CLI
jmeter -n -t test-plan.jmx -l results.jtl -e -o report-output/
# Parameters:
# -n: non-GUI mode
# -t: test plan file
# -l: log file
# -e: generate report
# -o: output directory
Best for: Established enterprise environments already using JMeter.
Locust — Python-Based, Open Source
Locust lets you write load tests in Python. It's popular with data science and backend teams already working in Python.
Basic Locust Test
# locustfile.py
from locust import HttpUser, task, between
class APIUser(HttpUser):
wait_time = between(1, 2) # Wait 1-2 seconds between tasks
def on_start(self):
"""Called when a user starts. Use for login/setup."""
response = self.client.post("/auth/login", json={
"username": "testuser",
"password": "testpass"
})
self.token = response.json()["token"]
@task(3) # Weight: this runs 3x more often than weight-1 tasks
def get_users(self):
self.client.get("/users", headers={
"Authorization": f"Bearer {self.token}"
})
@task(1)
def create_user(self):
self.client.post("/users", json={
"name": f"user-{id(self)}",
"email": f"user-{id(self)}@example.com"
}, headers={
"Authorization": f"Bearer {self.token}"
})
# Run with web UI
locust -f locustfile.py --host=https://api.example.com
# Run headless
locust -f locustfile.py --host=https://api.example.com \
--headless -u 50 -r 10 --run-time 2m
Locust includes a real-time web dashboard at localhost:8089 showing live metrics.
Distributed Load Generation
Locust supports distributed testing with a master/worker model:
# Start master
locust -f locustfile.py --master
# Start workers (can be on different machines)
locust -f locustfile.py --worker --master-host=master-ip
Best for: Python teams, teams that want a real-time web dashboard, flexible task weighting.
Artillery — YAML-First, Node.js-Powered
Artillery uses YAML for test configuration with JavaScript for custom logic. It's popular for its readable test format and good cloud support.
Basic Artillery Config
# load-test.yml
config:
target: "https://api.example.com"
phases:
- duration: 60
arrivalRate: 5
name: Warm up
- duration: 120
arrivalRate: 5
rampTo: 50
name: Ramp up load
- duration: 60
arrivalRate: 50
name: Sustained load
defaults:
headers:
Authorization: "Bearer {{ $processEnvironment.API_KEY }}"
scenarios:
- name: "API user flow"
flow:
- get:
url: "/users"
expect:
- statusCode: 200
- post:
url: "/users"
json:
name: "{{ $randomString() }}"
email: "{{ $randomString() }}@test.com"
expect:
- statusCode: 201
# Run
artillery run load-test.yml
# Run in cloud (Artillery Cloud)
artillery run --cloud load-test.yml
Best for: Teams that prefer YAML configuration, simple API flow testing, Node.js teams.
Gatling — Scala-Based, High Performance
Gatling scripts in Scala (or a Java API), compiles tests, and generates detailed HTML reports. It handles very high concurrency efficiently due to its Akka-based architecture.
// BasicSimulation.scala
class BasicSimulation extends Simulation {
val httpProtocol = http
.baseUrl("https://api.example.com")
.acceptHeader("application/json")
val users = scenario("Users")
.exec(
http("Get Users")
.get("/users")
.check(status.is(200))
)
setUp(
users.inject(
rampUsers(100).during(30.seconds)
)
).protocols(httpProtocol)
.assertions(
global.responseTime.percentile3.lt(500),
global.successfulRequests.percent.gt(99)
)
}
Best for: High-throughput testing (millions of requests), teams comfortable with Scala/JVM, enterprise environments needing detailed HTML reports.
Choosing the Right Tool
| Criteria | k6 | JMeter | Locust | Artillery | Gatling |
|---|---|---|---|---|---|
| Scripting language | JavaScript | XML/GUI | Python | YAML/JS | Scala |
| Setup difficulty | Easy | Hard | Medium | Easy | Medium |
| CI integration | Excellent | Good | Good | Good | Good |
| Real-time dashboard | Via cloud | Via GUI | Built-in | Via cloud | After test |
| Max throughput | High | High | Medium | Medium | Very High |
| Cloud execution | k6 Cloud | JMeter Cloud | Locust Cloud | Artillery Cloud | Gatling Cloud |
Our recommendation for new projects in 2025: k6
It has the best combination of developer experience, CI integration, and community. If your team is Python-first, Locust is the right alternative.
Interpreting Your Results
After running a load test, look at:
p95/p99 latency: The 95th and 99th percentile response times. These reveal the "worst case" experience most users never see in averages.
Error rate at load: Does it stay below 0.1%? 1%? At what VU count does it spike?
Throughput plateau: Find the maximum RPS before latency starts degrading. That's your saturation point.
Resource correlation: Compare your load test results with server CPU, memory, and database connection metrics during the same period.
Use DevPlaybook's free tools to streamline your development workflow: JSON Formatter, Base64 Encoder, Regex Tester, and more at devplaybook.cc.
Level Up Your Dev Workflow
Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.
🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.
Top comments (0)