Last modified: Oct 14, 2026

Load Test APIs with Locust

An API that works for one user can collapse under a thousand. Load testing reveals those breaking points before your customers find them.

Locust is a Python load testing tool that makes this easy. You describe user behavior in code, and Locust simulates thousands of concurrent requests.

This guide shows you how to load test APIs with Locust. You will write test scripts, add authentication, validate responses, and read the results.

Why Locust for API Testing

Locust tests are written in pure Python. There is no XML, no GUI editor, and no proprietary scripting language to learn.

That means you can use loops, conditionals, and any Python library inside your tests. You can pull data from a database, generate random payloads, or compute request signatures.

Locust also scales. A single process can simulate thousands of users. When you need more, you add worker machines and run in distributed mode.

The built-in web interface shows live statistics. You see requests per second, response times, and failure rates as the test runs.

If you have not set it up yet, start with this guide on how to install Locust in Python. Then come back here for the testing details.

Prerequisites

You need a few things before writing your first API test.

  • Python 3.10 or newer installed
  • Locust installed in a virtual environment
  • A running API you are allowed to test
  • Knowledge of the API endpoints and payloads

Never run load tests against a production system without permission. Start with a staging environment that mirrors production.

Writing a Basic API Test

Locust looks for a file named locustfile.py in your working directory. Create that file and define a user class.

 # locustfile.py from locust import HttpUser, task, between class ApiUser(HttpUser): """Simulates a client hitting the users API.""" # Pause 1 to 3 seconds between requests wait_time = between(1, 3) @task(3) def list_users(self): """GET the users list. Runs three times as often.""" self.client.get("/api/users") @task def create_user(self): """POST a new user with a JSON payload.""" payload = {"name": "Alice", "email": "[email protected]"} self.client.post("/api/users", json=payload) 

The between() function sets a random pause between tasks. This mimics real user pacing instead of hammering the server nonstop.

The self.client.get() method sends a GET request. Locust records the status code, response time, and payload size automatically.

The self.client.post() method sends JSON data to your endpoint. The weight of 3 on the list task means it runs more often than the create task.

Adding Authentication

Most real APIs require a token. You can log in once per simulated user and reuse the token for every request.

 from locust import HttpUser, task, between class AuthUser(HttpUser): wait_time = between(1, 2) def on_start(self): """Runs once when each simulated user starts.""" response = self.client.post("/api/login", json={ "username": "testuser", "password": "secret" }) token = response.json()["token"] self.client.headers.update({"Authorization": f"Bearer {token}"}) @task def get_profile(self): self.client.get("/api/profile") 

The on_start() method fires once per user, not once per request. That keeps login traffic realistic instead of flooding the auth endpoint.

After login, the token is added to the client headers. Every later request carries it automatically.

Store credentials in environment variables, not in the test file. Never commit real secrets to a repository.

Validating Responses

A 200 status code does not always mean success. An API can return 200 with an error message in the body.

Locust lets you inspect the response and mark it as a failure when the content is wrong.

 from locust import HttpUser, task, between class ValidateUser(HttpUser): wait_time = between(1, 3) @task def check_health(self): with self.client.get("/api/health", catch_response=True) as response: if response.status_code != 200: response.failure(f"Expected 200, got {response.status_code}") elif "ok" not in response.text: response.failure("Missing ok flag in body") else: response.success() 

The catch_response argument turns on manual result handling. Locust stops guessing and waits for your verdict.

The response.failure() method records the request as failed with your custom message. The response.success() method marks it as passed.

Without this block, Locust only checks the HTTP status. With it, you catch semantic bugs that status codes hide.

Simulating Multiple User Types

Real traffic comes from different clients. Some browse, some search, some upload. You can model each group with its own class.

 from locust import HttpUser, task, between class BrowserUser(HttpUser): weight = 3 wait_time = between(2, 5) @task def browse(self): self.client.get("/api/products") class MobileUser(HttpUser): weight = 1 wait_time = between(1, 2) @task def search(self): self.client.get("/api/search?q=phone") 

The weight attribute controls the mix. Three browser users spawn for every mobile user, matching a typical traffic split.

Each class can have its own wait time, headers, and task set. This produces a load profile that looks like your real audience.

Running Tests from the Command Line

For quick runs, use the web interface. For automation, run Locust headless.

 locust -f locustfile.py \ --host http://localhost:8000 \ --users 50 \ --spawn-rate 5 \ --run-time 2m \ --headless 

The users flag sets the peak concurrency. The spawn rate controls how fast new users join. The run time stops the test automatically.

Here is what the console output looks like during a short run.

 [2026-10-14 10:00:00,000] INFO/locust.main: Starting Locust 2.32.3 [2026-10-14 10:00:00,001] INFO/locust.runners: Ramping to 50 users at a rate of 5.00 per second Type Name # reqs # fails Avg Min Max req/s GET /api/users 1200 0 45 12 210 20.0 POST /api/users 400 0 62 20 300 6.7 GET /api/health 2400 0 8 3 45 40.0 Aggregated 4000 0 31 3 300 66.7 

The aggregated row is the number that matters most. It shows total throughput and the overall failure count.

Long-running API suites often grow into scheduled jobs. Teams that schedule Python tasks with Airflow can trigger a Locust run every night and alert on regressions.

Distributed Load Testing

One machine has limits. When you need tens of thousands of users, spread the load across workers.

Start the controller process first.

 locust -f locustfile.py --master --expect-workers 3 

Then start each worker on its own machine, pointing at the controller.

 locust -f locustfile.py --worker --master-host 192.168.1.10 

The controller aggregates statistics from all workers. The web interface and console output look the same as a single-process run.

Make sure every worker has the same test file and the same dependency versions. A mismatch causes silent errors.

Exporting and Reading Results

Save results to CSV for later analysis or reporting.

 locust -f locustfile.py --host http://localhost:8000 \ --users 100 --spawn-rate 10 --run-time 5m \ --headless --csv results 

This produces results_stats.csv, results_failures.csv, and results_stats_history.csv in your working directory.

The stats file holds percentile data. The failures file lists every distinct error with a count. That file is usually the most valuable output.

If you need to share results with stakeholders, you can also generate PDFs with ReportLab in a Python guide and turn the CSV numbers into a formatted report.

Common Pitfalls

Testing production. Always confirm you have permission. Load tests can take down a live service.

Ignoring response content. Status codes alone are not enough. Validate the body when it matters.

Unrealistic wait times. A wait time of zero creates unnatural traffic. Real users pause between actions.

Shared test data. If every user creates the same record, you measure database conflicts, not API performance.

Low client resources. If the load generator runs out of CPU, your numbers describe the generator, not the API.

No baseline. Run the same test before and after changes. Without a baseline, numbers have no meaning.

Best Practices

Start small and ramp up. A sudden spike from zero to 5000 users hides the point where things break.

Test one scenario at a time. Isolate endpoints so you know which one is slow.

Record the environment details with every run. API version, database size, and server count all affect the results.

Keep your test files organized. As your locustfile grows, split tasks into modules and keep imports clean.

Automate the runs. A load test that only happens once tells you very little.

Conclusion

Locust turns API load testing into regular Python code. You define users, tasks, and validation rules, and the tool handles the rest.

Start with one endpoint and a handful of users. Add authentication, response checks, and user classes as your confidence grows.

Then run it headless in a pipeline so every release gets measured. Compare results against a baseline and watch for regressions.

With Locust in your toolkit, performance stops being a guess. You get real numbers, repeatable tests, and clear answers before your users find the limits for you.