Last modified: Oct 24, 2026

Load Testing with Locust and pytest

Load testing shows how your app behaves under real traffic. It reveals slow endpoints, memory leaks, and crashes before users find them.

Locust is a Python load testing tool. You write user behavior in plain Python code. pytest is the most popular Python test runner. Together, they give you repeatable and readable performance tests.

This guide walks you through the setup, the core concepts, and a complete working example. It also covers common pitfalls and best practices.

Why Combine Locust and pytest?

Locust already ships with its own command line runner. So why add pytest?

pytest brings structure. You get fixtures, markers, assertions, and plugins. You can run load tests next to unit tests. You can also skip slow tests in CI and run them nightly.

pytest also gives you clean reporting. A failing load test becomes a failing test case. That makes performance part of your normal workflow.

Installing the Tools

Start with a virtual environment. Then install both packages.


python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
pip install locust pytest

Check the versions to make sure everything installed correctly.


locust --version
pytest --version

locust 2.29.1
pytest 8.1.1

Writing Your First Locust File

A Locust file defines users and the tasks they perform. Each user class extends HttpUser. Each task is a method decorated with @task.

Here is a simple example that hits a public test API.


from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    # Random wait between 1 and 3 seconds between tasks
    wait_time = between(1, 3)

    @task(3)
    def view_homepage(self):
        # Weight 3: runs three times more often
        self.client.get("/")

    @task(1)
    def view_about(self):
        self.client.get("/about")

The @task decorator accepts a weight. Higher weights mean the task runs more often. This mimics real user behavior better than equal weights.

The wait_time attribute simulates think time. Without it, users hammer the server unrealistically.

Running Locust Without a Web UI

Locust has a web interface by default. For automation, use headless mode.


locust -f locustfile.py --headless -u 50 -r 5 -t 30s --host https://example.com

This runs 50 users, spawns 5 per second, and stops after 30 seconds. The output shows request counts, failure rates, and response time percentiles.


Type     Name          # reqs   # fails   Avg   Min   Max   Median   req/s
GET      /             1200     0         45    10    210   40       39.8
GET      /about        400      0         38    9     190   35       13.2
Aggregated             1600     0         43    9     210   38       53.0

Pay attention to the 95th percentile. Averages hide slow outliers. Percentiles show what real users experience.

Wrapping Locust in a pytest Test

Locust exposes a programmatic API. You can start a MasterRunner or LocalRunner inside a pytest test. This gives you assertions on the results.

Here is a minimal pattern.


import pytest
from locust import HttpUser, task
from locust.env import Environment
from locust.stats import stats_printer
from locust.log import setup_logging

setup_logging("INFO", None)

class ApiUser(HttpUser):
    host = "https://example.com"

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

@pytest.fixture
def locust_env():
    # Build an environment and runner for the test
    env = Environment(user_classes=[ApiUser])
    env.create_local_runner()
    yield env
    env.runner.quit()

def test_load(locust_env):
    # Start 10 users and run for 10 seconds
    locust_env.runner.start(10, spawn_rate=5)
    locust_env.runner.greenlet.join(timeout=10)
    locust_env.runner.stop()

    stats = locust_env.stats.total
    # Assert the failure rate stays below 1 percent
    assert stats.fail_ratio < 0.01
    # Assert the 95th percentile stays under 500 ms
    assert stats.get_response_time_percentile(0.95) < 500

The fixture creates the runner. The test starts users, waits, and stops. Then it checks the aggregated stats.

The fail_ratio property returns failures divided by total requests. The get_response_time_percentile method returns the latency at a given percentile.

You can also assert on per-endpoint stats using locust_env.stats.entries. Each entry holds the name, method, and response time data.

Generating Realistic Test Data

Real load tests need real-looking payloads. Random usernames and emails make your test more meaningful. You can learn to generate test data with Faker in Python and plug it into your Locust tasks.

For example, a registration endpoint performs very differently with 10 users than with 10,000 unique users. Faker helps you avoid duplicates that hit cache and skew results.

If your API wraps an LLM, you should also test that path. Load testing AI endpoints is a hot topic. Read more about OpenAI function calling in Python to understand how those requests are shaped.

Testing with Assertions Inside Tasks

Locust does not fail a request on bad content by default. A 200 response with an error body still counts as success. You can fix that with the catch_response context manager.


from locust import HttpUser, task

class ApiUser(HttpUser):
    @task
    def check_health(self):
        with self.client.get("/health", catch_response=True) as response:
            if response.status_code != 200:
                response.failure(f"Bad status: {response.status_code}")
            elif "ok" not in response.text:
                response.failure("Health check did not return ok")
            else:
                response.success()

The catch_response context manager lets you mark a request as failed or successful. This makes your load tests catch logic bugs, not just HTTP errors.

You can combine this with pytest assertions. Failures recorded by Locust will show up in the aggregated stats your test checks.

Parameterizing Load Tests

pytest parametrization works well here. You can run the same scenario with different user counts or durations.


import pytest

@pytest.mark.parametrize("users,duration", [(10, 5), (50, 10), (100, 10)])
def test_scaling(locust_env, users, duration):
    locust_env.runner.start(users, spawn_rate=users // 5)
    locust_env.runner.greenlet.join(timeout=duration)
    locust_env.runner.stop()

    stats = locust_env.stats.total
    assert stats.fail_ratio == 0

This pattern reveals how response times grow as load increases. If latency jumps at 50 users, you have found your first bottleneck.

Common Pitfalls

Testing against production. Always use a staging environment. Load tests can take down real services.

Ignoring think time. Without wait_time, your test is a stress test, not a load test. Know which one you want.

Running from one machine. A single client machine becomes the bottleneck. Use distributed mode with --master and --worker flags for high loads.

Trusting averages. Always check percentiles. A 200 ms average with a 4-second p99 is a bad user experience.

Forgetting cleanup. Load tests create data. Add teardown steps or use a disposable database. This matters even more when you test data pipelines. If you produce and consume Kafka messages in Python, make sure your test topics are isolated.

Best Practices

Keep scenarios small and focused. One user class per flow. Name tasks clearly.

Tag tests with pytest markers like @pytest.mark.load. Then you can run them separately from unit tests.


pytest -m load --durations=10

The --durations=10 flag shows the slowest test phases. It helps you spot setup overhead in your load tests.

Track results over time. Store the JSON output from Locust and compare runs. A regression in p95 latency is easier to catch with history.

Finally, test text processing endpoints too. If your service tokenizes input, you can tokenization with NLTK in Python in a separate benchmark to isolate CPU cost from network cost.

Conclusion

Locust and pytest make a strong pair. Locust models real user behavior in Python. pytest adds structure, assertions, and reporting.

Start small. Write one user class. Run it headless. Then wrap it in a pytest test with a failure ratio and percentile assertion.

As your app grows, add parametrization and distributed workers. Watch the percentiles, not the averages. Your users will thank you.