Load testing isn’t just about seeing how many requests per second your server can handle; it’s about understanding how your users will actually interact with your system under pressure.
Let’s look at a live example. Imagine we’re load testing a simple e-commerce checkout flow.
# Simulate 100 concurrent users adding items to a cart
locust -f locustfile.py --host=http://localhost:8000 --users 100 --spawn-rate 10 --run-time 5m
# locustfile.py
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 5)
@task
def add_to_cart(self):
self.client.post("/cart/add", json={"item_id": "SKU123", "quantity": 1})
@task
def view_cart(self):
self.client.get("/cart")
@task
def checkout(self):
self.client.post("/checkout")
This locustfile.py defines a user that randomly performs add_to_cart, view_cart, and checkout actions, with a 1-5 second wait between them. Locust will then simulate 100 of these users, starting them gradually over 10 seconds (spawn-rate 10), for a total of 5 minutes.
The real power here is in designing scenarios that mimic user behavior. Instead of just hammering a single API endpoint, we’re simulating a sequence of actions a user might take: browsing, adding to cart, viewing the cart, and finally checking out. This reveals bottlenecks that might not appear when testing individual endpoints in isolation.
The Core Problem Load Testing Solves
The fundamental problem load testing addresses is performance degradation and failure under stress. Systems that work perfectly fine with a few dozen users can buckle and break when that number spikes to hundreds or thousands. This isn’t just about slow responses; it’s about outright failures, data corruption, and complete unavailability. Load testing quantifies this breaking point and helps you identify where and why it happens.
Internal Mechanics: How it Works
Load testing tools like Locust, k6, or JMeter work by spinning up multiple virtual users (threads or processes) on one or more machines. Each virtual user executes a predefined script that simulates user interactions. These scripts typically involve making HTTP requests, but can also simulate other protocols. The tool then collects metrics on response times, error rates, throughput (requests per second), and resource utilization (CPU, memory) on the system under test.
The key levers you control are:
- Number of Users: How many concurrent virtual users to simulate. This is the primary driver of load.
- Ramp-up/Spawn Rate: How quickly new users are introduced. A gradual ramp-up helps identify how the system handles increasing load over time, while a sudden spike tests its ability to absorb immediate surges.
- Duration: How long the test runs. This is crucial for identifying issues that only appear after sustained load (e.g., memory leaks).
- Scenario Complexity: The sequence of actions and the logic within the user scripts. This dictates the realism of the simulation.
- Think Times: The pauses between actions, mimicking human user behavior.
- Data Variation: Using different user credentials, product IDs, or search terms to avoid caching effects and test data-dependent logic.
The Surprising Truth About "Average" Metrics
Many load testing reports focus heavily on average response times. However, averages can be incredibly misleading. Averages can hide the fact that 95% of your users are experiencing sub-second responses, while the remaining 5% are waiting for 30 seconds or more. This is why focusing on percentile metrics (like p95 or p99 response times) is far more insightful. A p99 response time tells you the response time that 99% of your users are experiencing or better. If your p99 response time for a critical checkout API is 15 seconds, that’s a critical problem, even if the average is a respectable 500ms.
The next crucial step after designing your scenarios is analyzing the results to identify bottlenecks, often by correlating load test metrics with server-side monitoring data.