You can run Locust soak tests for days or even weeks to discover how your application behaves under sustained load.

Here’s a quick demo of Locust running a soak test:

# locustfile.py
from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 5)
    host = "http://localhost:8080"  # Target your application here

    @task
    def index(self):
        self.client.get("/")

    @task(2) # This task is twice as likely to be chosen
    def about(self):
        self.client.get("/about/")

Save this as locustfile.py. Then, start Locust from your terminal:

locust -f locustfile.py --host http://localhost:8080 --headless -u 100 -r 10 --run-time 7d

This command starts 100 users, with a spawn rate of 10 users per second, and runs the test for 7 days. The --headless flag means it runs without the web UI, which is typical for long-running soak tests.

A soak test isn’t just about hammering your system with a lot of users; it’s about observing its long-term behavior. You’re looking for subtle issues that only emerge after hours or days: memory leaks, connection pool exhaustion, disk space filling up, or performance degradation that creeps in over time. The goal is to find those "slow death" bugs that don’t appear in short, high-intensity load tests.

When you run a soak test, Locust’s master and worker architecture becomes particularly useful. You can distribute your load generators across multiple machines.

Master Node:

locust --master

Worker Nodes (run on separate machines):

locust --worker --master-host <master_ip_address>

Then, access the web UI on the master node (usually http://localhost:8089) to configure and start your test. You can specify the total number of users and the spawn rate. For soak tests, you’ll often use a moderate number of users that your system can sustain for a long time, rather than a massive spike.

The wait_time in your locustfile.py is crucial. For soak tests, you might want a slightly longer or more variable wait_time to simulate more realistic user behavior and avoid overwhelming the system too quickly, allowing issues to manifest gradually.

The client.get() and client.post() methods, among others, are your tools for interacting with your application. In a soak test, you’ll want to simulate the most common and critical user flows repeatedly. This means designing your locustfile.py to include tasks that represent typical user journeys, not just isolated requests.

For example, if your application involves a user logging in, browsing products, adding to a cart, and checking out, your locustfile.py should have tasks that chain these actions together logically.

# locustfile.py
from locust import HttpUser, task, between

class EcommerceUser(HttpUser):
    wait_time = between(2, 10)
    host = "http://localhost:8000"

    @task
    def browse_and_add_to_cart(self):
        with self.client.get("/products", catch_response=True) as response:
            if response.status_code == 200 and response.text:
                product_id = response.json()[0]['id'] # Assuming a JSON response
                with self.client.post(f"/cart/add/{product_id}", json={"quantity": 1}, catch_response=True) as add_response:
                    if add_response.status_code != 201:
                        add_response.failure(f"Failed to add to cart: {add_response.status_code}")

    @task(2) # More likely to browse and add
    def view_product_details(self):
        with self.client.get("/products/123", catch_response=True) as response:
            if response.status_code != 200:
                response.failure(f"Failed to view product details: {response.status_code}")

The catch_response=True is vital in soak tests. It allows you to inspect the response and manually mark requests as failures if they don’t meet your criteria (e.g., wrong status code, missing data), even if the HTTP request itself didn’t technically error out. This gives you much finer-grained control over what constitutes a "failed" request in your long-running test.

When running a soak test, you’re not just looking at request counts and response times. You need to correlate Locust’s output with your application’s own monitoring. This means having metrics dashboards for your servers (CPU, memory, network, disk I/O), database performance, and any other critical components. Look for trends: does memory usage steadily increase? Does response time slowly creep up? Does the error rate, even if low, show a persistent upward trend?

One of the most powerful, yet often overlooked, aspects of Locust for soak testing is its ability to simulate different user behaviors over time. By carefully crafting your locustfile.py with a variety of tasks and varied wait_time settings, you can create a more realistic workload that mimics how users interact with your application over extended periods. This includes simulating periods of low activity interspersed with bursts of higher activity, or even simulating users logging out and back in. The key is to avoid a uniform, constant load, which is rarely how real-world applications are used.

After a soak test, you’ll want to analyze the collected data. Locust provides detailed statistics, but for long-duration tests, you’ll likely be exporting this data (e.g., to InfluxDB and Grafana) for more in-depth analysis and historical trending.

The next step after a successful soak test is often to run a targeted performance test based on the bottlenecks you discovered.

Want structured learning?

Take the full Locust course →