The most surprising thing about load testing authentication is that the real bottleneck is rarely the authentication mechanism itself; it’s the downstream services that rely on that authentication being valid.

Let’s see it in action. Imagine we’re testing a web API that requires a JWT for every request. A user logs in, gets a token, and then uses that token for subsequent API calls.

from locust import HttpUser, task, between

class AuthenticatedUser(HttpUser):
    wait_time = between(1, 5)
    host = "http://localhost:8080"  # Replace with your API host

    def on_start(self):
        # Simulate login and get the token
        login_response = self.client.post("/api/auth/login", json={"username": "testuser", "password": "password123"})
        if login_response.status_code == 200:
            self.auth_token = login_response.json()["access_token"]
            print("Login successful, token acquired.")
        else:
            print(f"Login failed: {login_response.status_code} - {login_response.text}")
            self.auth_token = None # Ensure we don't proceed if login fails

    @task
    def get_user_profile(self):
        if self.auth_token:
            # Use the acquired token for subsequent requests
            self.client.get(
                "/api/users/me",
                headers={"Authorization": f"Bearer {self.auth_token}"}
            )
        else:
            print("Skipping task, no auth token available.")

    # Add more tasks that require authentication
    @task
    def get_user_orders(self):
        if self.auth_token:
            self.client.get(
                "/api/orders",
                headers={"Authorization": f"Bearer {self.auth_token}"}
            )

This AuthenticatedUser class defines a user that first logs in (on_start) and then uses the obtained auth_token in subsequent tasks (get_user_profile, get_user_orders). Locust will spawn multiple instances of this user, each performing its own login and then executing its authenticated tasks.

The problem we’re trying to solve is understanding how many concurrent users our system can handle while they are authenticated and actively using resources. This isn’t just about how fast the /api/auth/login endpoint responds. It’s about the combined load of login requests and all the requests that follow, each carrying that authentication credential.

Internally, Locust handles this by instantiating HttpUser classes. The on_start method runs once per user instance when it’s spawned. Subsequent @task methods are executed repeatedly according to the wait_time and the user’s task weight. For authenticated flows, the crucial part is how the self.client object (an instance of HttpSession) is used to maintain state, like the auth_token captured from the initial login response. This token is then passed in the headers of subsequent requests.

The levers you control are primarily within the HttpUser class:

  • host: The base URL of the service under test.
  • wait_time: Defines the pause between user actions. between(1, 5) means each user waits 1 to 5 seconds before executing their next task.
  • on_start: This is where you’d put your authentication logic. This could involve POSTing credentials to a /login endpoint, extracting a token from the response, or even handling OAuth flows.
  • @task methods: These represent the actions authenticated users take. You’d define tasks for fetching data, performing actions, etc., ensuring each task includes the necessary authentication headers.
  • Task weights: By assigning weights to different @task methods, you can control the proportion of users performing specific authenticated actions. For example, if 80% of users are browsing their profile and 20% are placing orders, you’d reflect that in the weights.

When you run Locust, it spawns N instances of AuthenticatedUser. Each instance will execute on_start once. If on_start successfully retrieves a token, that user instance will then repeatedly execute its @task methods, always including the Authorization header. The HttpSession object within Locust is smart enough to manage cookies and headers across requests for a single user instance, making it ideal for simulating stateful, authenticated sessions.

The common pitfall is to focus only on the login endpoint’s latency. However, the real pressure often comes from the database queries, cache lookups, or other service calls that happen after the authentication check but before the requested data is returned. A token might be validated in milliseconds, but if that validation triggers a slow SELECT * FROM users WHERE id = ? query that isn’t properly indexed, your system will buckle. You need to measure the entire round trip for authenticated requests, not just the authentication part.

The next concept you’ll run into is handling token expiration and refresh mechanisms within your load tests.

Want structured learning?

Take the full Locust course →