26.12. Load Testing
Table of Contents
Why Load Testing Matters
Load testing checks how your backend behaves when many users use it at the same time. You do not test if a single request works, you test if hundreds or thousands of requests per second still work correctly and fast enough.
Typical goals:
- Find the maximum number of users or requests your system can handle.
- Discover performance bottlenecks, for example slow database queries.
- Check that the system remains stable under heavy traffic.
- Verify scaling strategies, caching, and configuration.
Key idea: Load testing focuses on performance and capacity under expected real-world traffic, not just on correctness of features.
You usually run load tests before a big launch, marketing campaign, or feature release, and regularly in CI/CD for critical systems.
Basic Load Testing Concepts
Users, Requests, and Throughput
When talking about load testing, you will see these terms again and again:
| Term | Meaning | Example |
|---|---|---|
| Virtual user | A simulated user performing actions against your API | 500 virtual users calling your API |
| Request | One HTTP call to an endpoint | GET /products?page=1 |
| Throughput | Number of requests processed per time unit | 1000 requests per second (RPS) |
| Concurrency | How many requests or users are active at the same time | 300 concurrent users |
| Latency | Time from sending a request until the response is received | 250 ms |
| Response time | Often used as a synonym for latency | 250 ms |
| Duration | How long you run the load test | 10 minutes |
We often care not just about the average response time but also the percentiles.
- p50: 50% of requests are faster than this value (median).
- p95: 95% of requests are faster.
- p99: 99% of requests are faster.
Example:
- p50 = 100 ms, p95 = 400 ms, p99 = 1000 ms
This means most users get fast responses, but a few have slow ones.
Important: A system can have a good average but terrible tail latencies. Always look at p95 and p99, not only the mean.
Types of Performance Tests
Load testing is part of a broader family of performance tests:
| Type | Purpose |
|---|---|
| Load testing | Test behavior under expected normal or slightly higher than normal load |
| Stress testing | Push the system beyond its limits until it fails |
| Soak testing | Run with medium, realistic load for a long time to detect memory leaks or resource issues |
| Spike testing | Apply sudden, very large load to see if the system survives sudden traffic spikes |
Focus here is load testing, but it helps to know the terms, because tools support multiple modes.
What to Test in a Backend
You can target many parts of your backend:
- REST API endpoints
- Heavy endpoints, for example
/search,/checkout. - Frequently used endpoints, for example
/products,/me. - Authentication flows
- Login, token refresh, registration.
- Database-heavy operations
- Reports, analytics, large queries.
- Background-triggering endpoints
- Endpoints that enqueue jobs, for example
/orders.
You usually do not test all endpoints. Instead, you pick:
- The endpoints with the highest traffic.
- The endpoints that use the most CPU or database time.
- Critical flows: login, add to cart, checkout, payment.
Example test focus for an e-commerce backend:
| Flow | Example endpoints |
|---|---|
| Browsing products | GET /products, GET /products/{id} |
| Cart and checkout | POST /cart/items, POST /checkout |
| User account | POST /auth/login, GET /me |
| Admin operations | POST /admin/products, GET /admin/orders?status=pending |
Defining Performance Goals
Before you run any tool, you must define what “good performance” means for your application.
Service Level Objectives (SLOs)
You can describe performance targets as SLOs. For example:
- 95% of responses for
/productsmust be faster than 200 ms. - 99% of
/checkoutresponses must be faster than 800 ms. - Error rate must stay below 0.5% during normal load.
In math form:
- Let $T$ be response time.
- Requirement example: $$P(T \le 0.8\ \text{seconds}) \ge 0.99$$
This means: at least 99% of requests complete in 0.8 seconds or less.
Rule: Never run load tests without clearly defined targets. You need SLOs to know if the result is acceptable or not.
Determining Expected Load
You need an estimate of:
- Expected concurrent users.
- Expected requests per second.
- Expected growth over time.
Example:
- You expect 1000 active users at peak.
- On average, each user sends 1 request every 3 seconds.
- So expected RPS: $$\text{RPS} = \frac{1000\ \text{users}}{3\ \text{sec}} \approx 333\ \text{RPS}$$
You may set your load test target at 1.5x or 2x this value to have a safety margin. For example, test at 600 RPS.
Building Realistic Load Scenarios
Realistic tests try to imitate real user behavior, not just call one endpoint in a tight loop.
User Journeys
A user journey is a sequence of actions that a typical user performs.
Example: “Anonymous browsing” journey
GET /products?page=1GET /products/123GET /products/456GET /categories/electronics
Example: “Authenticated shopping” journey
POST /auth/loginGET /products?category=phonesPOST /cart/itemsPOST /checkout
A load testing tool can simulate thousands of users following these journeys.
Think Time and Pauses
Real users do not click instantly. They read pages, think, move the mouse. If you send requests without delay, the load will be unrealistically high.
You introduce think time between actions, for example:
- Wait 2 to 5 seconds between page views.
- Wait 0.5 to 1 second between API calls that represent “user clicks.”
Example in pseudo-code:
def user_journey(client):
client.get("/products?page=1")
sleep(random_between(2, 5))
client.get("/products/123")
sleep(random_between(1, 3))
client.post("/cart/items", json={"product_id": 123, "quantity": 1})
sleep(random_between(1, 2))
client.post("/checkout")This creates more realistic traffic patterns.
Mix of Endpoints
In production, some endpoints are called more than others. For example:
/productsmight receive 70% of traffic./cart20%./checkout10%.
Many tools allow you to define weights or define more users for some flows than others.
Example mix:
| Flow | Share of users | Rough share of requests |
|---|---|---|
| Browsing | 60% | ~60% |
| Browsing + cart | 30% | ~30% |
| Browsing + cart + checkout | 10% | ~10% |
This helps you see real bottlenecks, not only synthetic ones.
Popular Load Testing Tools
You do not have to write your own load simulation. There are many tools.
Some common ones:
| Tool | Language / Interface | Style | Good for |
|---|---|---|---|
ab | CLI | Very simple | Quick single-endpoint tests |
wrk | CLI + Lua | High performance | Heavy load, scripting |
| JMeter | GUI / XML | Enterprise | Complex test plans, older setups |
| Locust | Python | Code-based | Python backends, user-journey modeling |
| k6 | JavaScript + CLI | Code-based | CI/CD integration, modern workflows |
For a Python-based backend, Locust is very popular, since you can describe tests as Python code.
Example: Simple CLI Load Test with `ab`
ab (ApacheBench) is a very small tool often available on Linux.
Example command:
ab -n 1000 -c 50 https://api.example.com/productsMeaning:
-n 1000total number of requests.-c 50concurrency, 50 requests active at the same time.- URL is the endpoint to test.
ab prints:
- Requests per second.
- Time per request.
- Distribution of response times.
Limitations:
- One endpoint at a time.
- No complex user journeys.
- Not ideal for very flexible tests, but enough for a quick first look.
Example: Load Test with Locust (Python)
Locust lets you define user classes and tasks in Python.
Basic Locust Script
Install Locust:
pip install locust
Create locustfile.py:
from locust import HttpUser, task, between
class BrowsingUser(HttpUser):
wait_time = between(1, 3) # think time between tasks
@task(3) # higher weight: more frequent
def list_products(self):
self.client.get("/products?page=1")
@task(1)
def view_product(self):
self.client.get("/products/123")Run Locust:
locust -f locustfile.py --host=https://api.example.com
Then open the web UI (default at http://localhost:8089), and:
- Set number of users, for example 200.
- Set spawn rate, for example 10 users per second.
- Start the test and watch graphs and metrics.
Adding Authenticated Flows
Example with login and authenticated requests:
from locust import HttpUser, task, between
class AuthenticatedUser(HttpUser):
wait_time = between(1, 3)
def on_start(self):
# called when a simulated user starts
response = self.client.post(
"/auth/login",
json={"email": "test@example.com", "password": "secret"},
)
token = response.json()["access_token"]
self.headers = {"Authorization": f"Bearer {token}"}
@task
def get_profile(self):
self.client.get("/me", headers=self.headers)
@task
def list_orders(self):
self.client.get("/orders", headers=self.headers)This lets you test authorized endpoints under load.
How to Run Load Tests Safely
Never Attack Production by Surprise
Load tests can overload your system and affect real users. You must be careful.
Recommended:
- Test against a staging or pre-production environment that is similar to production.
- If you must test production, announce it, schedule it in a low-traffic window, and set safe limits.
Rule: Do not run large, uncontrolled load tests against production without approval and monitoring. You can cause an outage.
Control Test Parameters
Typical parameters you choose:
- Number of virtual users, for example 100, 500, 2000.
- Ramp-up time or spawn rate, for example 10 users per second.
- Test duration, for example 10 to 30 minutes for basic load.
Example plan:
- Start with 100 users, run 5 minutes.
- Then 300 users, run 10 minutes.
- Then 600 users, run 10 minutes.
Observe behavior at each step.
Monitoring During Load Tests
Load test metrics are not enough. You also need backend-side metrics.
You should monitor:
| Metric type | Examples |
|---|---|
| CPU | CPU usage per instance |
| Memory | Memory usage, swap usage |
| Disk | Disk I/O, disk latency, free space |
| Network | Incoming and outgoing traffic, errors |
| Database | Query time, CPU, locks, active connections |
| Application metrics | Requests per second, errors, latency percentiles |
During the test, watch for:
- CPU stuck at 100%.
- Increased memory usage over time.
- Database connection pool exhaustion.
- Error spikes in logs.
Example: If CPU reaches 100% and latency increases sharply at around 400 RPS, that is probably your current capacity limit with this configuration.
Interpreting Results
After the test you have two sides of data:
- Metrics from the load tool (latency, throughput, errors).
- Metrics from infrastructure and application monitoring.
You look for:
- Throughput vs latency curve
As throughput (RPS) increases, response time usually rises.
- Good region: latency stable or slowly increasing.
- Bad region: latency grows rapidly, for example from 200 ms to 2000 ms.
- Error rate
- Timeouts.
- 5xx server errors.
- Application-specific error responses.
- Resource saturation
- CPU near 100%.
- Database connection pool maxed out.
- Disk or network saturation.
Example interpretation:
- Up to 300 RPS: p95 = 200 ms, no errors.
- 400 RPS: p95 = 350 ms, few 5xx errors.
- 500 RPS: p95 = 1200 ms, many 5xx, CPU at 100%.
Conclusion:
- Your safe capacity is somewhere around 300 to 350 RPS with current hardware and configuration.
- You need optimization or scaling to handle 500 RPS reliably.
Common Bottlenecks Revealed by Load Tests
Load tests often reveal:
- Slow database queries
- Missing indexes.
SELECT *over large tables.- Expensive joins on big datasets.
- Insufficient connection pooling
- Too few connections to the database.
- Too many connections causing database contention.
- Lock contention
- Many requests trying to update the same rows.
- Inefficient code
- N+1 database queries (multiple queries inside loops).
- Heavy JSON serialization, expensive computations.
- Missing caching
- Recomputing expensive results for every request.
- External API dependencies
- Remote service or payment provider responds slowly.
Load testing helps you see these problems in a realistic scenario.
Iterative Tuning Process
Load testing is not a one-time activity. You normally follow a loop:
- Baseline test
- Run a test with current version.
- Record metrics, capacity, latency.
- Identify bottlenecks
- Use profiling, logs, database query analysis.
- Optimize
Examples:
- Add indexes.
- Add caching.
- Fix N+1 queries.
- Enable compression.
- Tune connection pool sizes.
- Retest
- Run the same test scenario again.
- Compare before and after.
- Repeat
You always keep a history of test results to track improvements.
Example:
- Before optimization: capacity ~300 RPS, p95 400 ms.
- After query optimization and caching: capacity ~700 RPS, p95 250 ms.
Load Testing in CI/CD
For critical APIs, you can add automated load tests in your CI/CD pipeline. This is not always full-scale, but at least ensures you did not introduce huge regressions.
Possible approach:
- On each merge to main:
- Deploy to a temporary environment.
- Run a short load test, for example:
- 50 users.
- 5 minutes.
- Fail the pipeline if:
- Error rate > 1%.
- p95 latency worse than some threshold.
For bigger, more expensive tests:
- Run them on a schedule, for example nightly or weekly.
- Or run them before major releases.
Practical Tips and Gotchas
- Warm-up phase
- Many systems are slower at the very beginning due to:
- Cold caches.
- JIT compilation.
- Lazy connections.
- Either ignore the first minutes or run a warm-up step before measurements.
- Use realistic data
- Test database size should be similar to production.
- Using a tiny dataset can hide performance problems that appear with large data.
- Reset state between big tests
- Clean database or reset it to a known state.
- Ensure there is no leftover test data that pollutes results.
- Respect external services
- Do not accidentally DDOS third-party APIs (payment providers, email services).
- Use mock services or sandbox environments.
- Document each test
Write down:
- Test date and time.
- Code version.
- Configuration (instance size, database settings).
- Load pattern (users, RPS).
- Results summary.
This makes it easier to compare and track performance over time.
Summary
Load testing verifies that your backend can handle realistic traffic while meeting defined performance goals. You:
- Define targets in terms of latency percentiles, throughput, and error rates.
- Build realistic user journeys with think times and a mix of endpoints.
- Use tools like Locust, k6, JMeter, or simple CLI tools such as
ab. - Monitor both the load tool metrics and backend infrastructure metrics.
- Identify bottlenecks, optimize, and repeat.
With regular load testing, you reduce the risk of slowdowns or outages when real users hit your application in large numbers.
Views: 8
KAHIBARO