Load testing individual microservices in isolation is a common pitfall; the real stress emerges when they interact under load.

Here’s how to set up Locust to test your entire microservices architecture as a cohesive unit, rather than just each component’s button press.

from locust import HttpUser, task, between

class MicroserviceUser(HttpUser):
    wait_time = between(1, 5) # Simulate user think time

    @task
    def user_workflow(self):
        # Simulate a user initiating a process that spans multiple services
        response_user = self.client.post("/users", json={"username": "testuser"})
        if response_user.status_code == 201:
            user_id = response_user.json()["id"]

            # Service 2: Order creation
            response_order = self.client.post("/orders", json={"user_id": user_id, "item": "widget"})
            if response_order.status_code == 201:
                order_id = response_order.json()["id"]

                # Service 3: Payment processing (dependent on order)
                response_payment = self.client.post("/payments", json={"order_id": order_id, "amount": 10.00})
                if response_payment.status_code == 200:
                    print(f"Successfully processed order {order_id} for user {user_id}")
                else:
                    print(f"Payment failed for order {order_id}: {response_payment.status_code}")
            else:
                print(f"Order creation failed for user {user_id}: {response_order.status_code}")
        else:
            print(f"User creation failed: {response_user.status_code}")

    # You can add more tasks to simulate other workflows
    @task
    def browse_products(self):
        self.client.get("/products?category=electronics")

This MicroserviceUser class defines a single user type that orchestrates a sequence of requests. The user_workflow task simulates a user creating an account, placing an order, and then processing a payment. Crucially, it chains these actions, meaning the success of the payment depends on the order creation, which in turn depends on user creation. This is how you represent inter-service dependencies in Locust.

The problem this solves is the "siloed testing" trap. If you test each service independently with 1000 requests per second, you might think your system is robust. But when Service A calls Service B, and Service B calls Service C, the aggregate load on Service C is not just the direct requests to it, but also the requests it receives indirectly through its dependencies. A bottleneck in Service B might cause a cascade of failures or extreme slowdowns that wouldn’t appear in isolated tests. Locust, by simulating these chained user journeys, exposes the systemic load.

Internally, Locust’s HttpUser and its client attribute are designed for this. The client object, an instance of HttpSession, handles request execution, response parsing, and most importantly, it maintains cookies and session state across requests within a single user’s execution. When you chain requests like in user_workflow, each subsequent request is made within the context of the user’s current session, mimicking real user behavior. The wait_time parameter is vital; it injects realistic pauses between user actions, preventing users from hammering the system unrealistically fast and allowing you to observe how the system behaves under sustained, but not instantaneous, concurrent load.

The real magic happens when you start multiple Locust users. Each user instance runs independently, executing its defined tasks. If you run 1000 MicroserviceUser instances, you’re not just sending 1000 POST /users requests, 1000 POST /orders requests, and 1000 POST /payments requests. You’re sending 1000 sequences of these requests, each sequence representing one user’s complete journey. This means Service C will receive requests from all users who reach the payment stage, Service B will receive requests from all users who reach the order stage, and Service A will receive requests from all users starting their journey. This accurately models the cumulative load and the cascading effects of latency or failures across your microservices.

When you configure Locust, you’ll typically point it at the API Gateway or the entry point that orchestrates these microservices. For example, if your API Gateway is running at http://localhost:8000, you’d set the Host in Locust’s UI to http://localhost:8000. Locust will then make all requests defined in your HttpUser classes to this host. If your services are behind this gateway, Locust naturally tests the integrated system. If you have direct service-to-service communication that bypasses the gateway for certain internal workflows, you might need to adjust your HttpUser’s host attribute or use separate HttpUser classes with different host configurations, though testing through a common entry point is usually more representative of external user load.

A subtle but critical aspect of simulating real user workflows is handling asynchronous operations. If Service B triggers an asynchronous task in Service C (e.g., via a message queue) and the user’s workflow needs to poll for its completion, your Locust task would need to incorporate polling logic. This might involve a loop with time.sleep and repeated calls to check a status endpoint, ensuring your test accurately reflects the actual user experience, including waiting for background processes.

The next hurdle you’ll encounter is debugging distributed transaction failures during high load.

Want structured learning?

Take the full Locust course →