Locust

repository·master·Indexed 12 days ago

https://github.com/locustio/locust

An open-source, distributed load testing tool that allows developers to define complex user scenarios using plain Python code. It is highly scalable and supports various protocols beyond HTTP, including MQTT, PostgreSQL, MongoDB, and vector databases like Milvus and Qdrant. It features a web UI and a headless mode for automated runs, and can be deployed on AWS via Terraform or integrated into Azure Load Testing.

Tokens
54.1K
Snippets
193
Records
254
Agent score
96%

What's inside Locust

  1. Overview of Locust features

    master

    Locust is an open-source, developer-friendly performance and load testing tool.

    Core capabilities include:

    • Protocol Agnostic: While primarily used for HTTP, you can test any system by writing a custom client.
    • Scalability: Uses an event-based architecture (via gevent) to support hundreds of thousands of concurrent users across multiple machines in a distributed setup.
    • Real-time Monitoring: Provides a web-based UI to view throughput, response times, and error rates in real-time. You can also adjust the load dynamically while the test is running.
    • Extensibility: Designed to be 'hackable' using standard Python. You can implement custom load shapes, custom reporting (e.g., to Grafana), or wrap API calls to handle specific protocol requirements.
  2. What is Locust?

    master

    Locust is an open source performance and load testing tool designed for HTTP and other protocols. It uses a developer-friendly approach where test scenarios are defined using regular Python code rather than XML, binary formats, or GUI-based configurations.

    Key characteristics include:

    • Python-based testing: Write scenarios using standard Python programming constructs (loops, conditionals, calculations). Because Locust runs users inside lightweight greenlets, you can write tests as normal blocking Python code without needing callbacks.
    • Scalability: Built on an event-based architecture (using gevent), allowing a single process to handle thousands of concurrent users. It supports distributed testing across multiple machines to simulate hundreds of thousands of concurrent users.
    • Execution Modes: Tests can be run via the command line (ideal for CI/CD) or through a web-based UI that provides real-time monitoring of throughput, response times, and errors. The UI also allows for dynamic load adjustment during a running test.
    • Extensibility: The pluggable architecture allows you to test almost any system or protocol by writing a custom client, or by using community-created plugins.
  3. Customize Locust UI tabs and components

    master

    You can customize the UI in two ways:

    1. Extended Tabs: By default, these render a table based on extendedTables and extendedStats. However, you can provide a custom React component to render any UI within that tab.
    2. The tabs prop: This allows you to completely control the order and visibility of tabs. You can use tabConfig to include base tabs (like stats or charts) and mix them with your own custom tab objects.
    import { IRootState } from "locust-webui";
    import { useSelector } from "react-redux";
    
    // 1. Custom component using Redux state
    function MyCustomTab() {
        const extendedStats = useSelector(
            ({ ui: { extendedStats } }: IRootState) => extendedStats
        );
        return <div>{JSON.stringify(extendedStats)}</div>;
    }
    
    // 2. Using extendedTabs with a custom component
    const extendedTabs = [{
        title: "Content Length",
        key: "content-length",
        component: MyCustomTab
    }];
    
    // 3. Using the tabs prop for complete control
    import LocustUi, { tabConfig } from "locust-ui";
    
    const tabs = [
        tabConfig.stats,
        tabConfig.charts,
        {
            title: "Custom Tab",
            key: "custom-tab",
            component: MyCustomTab,
        },
    ];
    
    function App() {
        return <LocustUi tabs={tabs} />;
    }
  4. Use non-Python workers with Boomer (Go) or Locust4j (Java)

    master

    Locust master and worker nodes communicate using msgpack messages. This allows you to write User tasks in languages other than Python. You can use specialized worker runner libraries to execute your tasks and report results back to the Locust master:

  5. Understand OpenTelemetry auto-instrumentation in Locust

    master

    Locust provides automatic instrumentation for the requests library. This means that when using the HttpUser class, HTTP requests will automatically generate spans and metrics without additional configuration.

    User ClassInstrumented Library
    HttpUserrequests

    If you use other libraries (e.g., database clients or messaging libraries), you must manually set up instrumentation using the OpenTelemetry Python SDK.

  6. Increase performance with FastHttpUser

    master

    Locust's default HttpUser uses python-requests, which is feature-rich but can be CPU-intensive for very high throughput. For scenarios requiring maximum requests per second (RPS) on limited hardware, use FastHttpUser.

    FastHttpUser uses geventhttpclient instead of python-requests. It can increase the maximum RPS on a given hardware by as much as 5x-6x.

    Key considerations:

    • Response Times: FastHttpUser does not make individual requests faster; it only makes the load generator more efficient. Response times should be nearly identical to HttpUser as long as the load generator's CPU is not overloaded.
    • CPU Monitoring: Check Locust's console output; it will log a warning if the load generator is limited by CPU.
    • Scaling: For optimal performance, run one Locust process per CPU core.
    from locust import task, FastHttpUser
    
    class MyUser(FastHttpUser):
        @task
        def index(self):
            response = self.client.get("/")
  7. How to extend Locust to test other protocols

    master

    Locust is extensible beyond HTTP/HTTPS. To test custom protocols (like XML-RPC or gRPC), you wrap the protocol's library and manually trigger the request event from locust.event.Events.request after each call. This allows Locust to track success/failure and response times.

    Important: Gevent Compatibility For Locust to scale, the library you use must be compatible with gevent monkey-patching.

    • Pure Python libraries (using socket or subprocess) usually work out of the box.
    • C-based libraries (compiled code) cannot be patched by gevent and will block the entire process, limiting you to a single User per worker.
    • Workaround: For libraries like psycopg2, use specialized tools like psycogreen to enable gevent support.
  8. Configure User wait_time

    master

    The wait_time attribute defines how long a user waits after completing a task before picking the next one. If not specified, the next task runs immediately.

    Available wait time types:

    • constant(seconds): A fixed amount of time.
    • between(min, max): A random time between a min and max value.
    • constant_throughput(tasks_per_second): An adaptive time ensuring the task runs at most X times per second.
    • constant_pacing(seconds): An adaptive time ensuring the task runs at most once every X seconds (inverse of throughput).

    You can also implement a custom wait_time(self) method on your class for complex logic.

    from locust import User, task, between
    
    class MyUser(User):
        wait_time = between(0.5, 10)
    
        @task
        def my_task(self):
            print("executing my_task")
  9. How MarkovTaskSet models probabilistic user flows

    master

    MarkovTaskSet allows you to define a probabilistic sequence of tasks using a Markov chain. Instead of tasks being chosen randomly based on global weights, the next action is determined by the current state (the current task) and defined transition probabilities.

    Requirements & Limitations:

    • Reachability: All tasks must eventually be reachable from the first task.
    • Transitions: At least one task must have transitions defined.
    • Tags: Tags are not supported in MarkovTaskSet because they can invalidate the Markov chain logic.

    Transition Decorators:

    1. @transition(task_name, weight=1): Defines a single transition to a specific task.
    2. @transitions(weights): Defines multiple possible next steps.
      • weights can be a dictionary: {"task_a": 3, "task_b": 1}
      • weights can be a list of names or tuples: ["task_a", ("task_b", 2)]
    from locust import User, constant
    from locust.user.markov_taskset import MarkovTaskSet, transition, transitions
    
    class ShoppingBehavior(MarkovTaskSet):
        wait_time = constant(1)
        
        @transition("view_product")
        def browse_catalog(self):
            self.client.get("/catalog")
        
        @transitions({
            "add_to_cart": 3,  # 60% chance
            "browse_catalog": 1, # 20% chance
            "checkout": 1      # 20% chance
        })
        def view_product(self):
            self.client.get("/product/1")
        
        @transitions(["view_product", "checkout"])
        def add_to_cart(self):
            self.client.post("/cart/add", json={"product_id": 1})
        
        @transition("browse_catalog")
        def checkout(self):
            self.client.post("/checkout")
    
    class ShopperUser(HttpUser):
        host = "http://localhost"
        tasks = {ShoppingBehavior: 1}
  10. How custom load shapes work with LoadTestShape

    master

    When standard user counts and spawn rates are insufficient, you can use a LoadTestShape class to gain full control over the load profile (e.g., creating spikes or custom ramps).

    To implement a custom shape:

    1. Inherit from LoadTestShape in your locust file.
    2. Implement a tick() method. This method is called approximately once per second and must return a tuple: (user_count, spawn_rate). To stop the test, return None.
    3. Use self.get_run_time() within tick() to determine how long the test has been running.
    4. (Optional) Use self.get_current_user_count() to monitor active users, which is useful for ensuring a stage is fully loaded before moving to the next.

    If you want to create a base class for reuse without Locust attempting to use it as the active shape, set abstract = True.

    class MyCustomShape(LoadTestShape):
        time_limit = 600
        spawn_rate = 20
        
        def tick(self):
            run_time = self.get_run_time()
    
            if run_time < self.time_limit:
                # User count rounded to nearest hundred.
                user_count = round(run_time, -2)
                return (user_count, self.spawn_rate)
    
            return None
  11. Use event hooks to extend Locust

    master

    Locust provides an event system that allows you to hook into various stages of the test lifecycle. You can use @events.<event_name>.add_listener to register functions that execute when specific events occur.

    Common use cases include:

    • events.request: Triggered after a request is completed. Useful for custom logging or monitoring.
    • events.test_start / events.test_stop: Triggered at the beginning and end of a test run. Useful for setup/cleanup.
    • events.init: Triggered when Locust is initialized. Useful for adding web routes or spawning background tasks.
    • events.init_command_line_parser: Used to add custom CLI arguments.

    Note on Distributed Mode: When running in distributed mode, use isinstance(environment.runner, MasterRunner) to ensure certain logic (like setup or monitoring) only runs on the master node and not on every worker.

    from locust import events
    from locust.runners import MasterRunner
    
    @events.request.add_listener
    def my_request_handler(request_type, name, response_time, response_length, response, 
                           context, exception, start_time, url, **kwargs):
        if exception:
            print(f"Request to {name} failed with exception {exception}")
        else:
            print(f"Successfully made a request to: {name}")
    
    @events.test_start.add_listener
    def on_test_start(environment, **kwargs):
        if not isinstance(environment.runner, MasterRunner):
            print("Beginning test setup on worker")
  12. Manage User weights and fixed counts

    master

    When multiple User classes are defined in a locustfile, you can control how many of each are spawned:

    • Weighting: Use the weight attribute to make certain user types more frequent. For example, weight = 3 makes that class 3x more likely to be spawned than a class with weight = 1.
    • Fixed Count: Use the fixed_count attribute to spawn an exact number of users of a specific type. These users are spawned before weighted users and ignore the weight attribute.

    To specify which classes to run from the CLI:

    $ locust -f locust_file.py WebUser MobileUser
    class WebUser(User):
        weight = 3
    
    class MobileUser(User):
        weight = 1
    
    class AdminUser(User):
        fixed_count = 1
        @task
        def restart_app(self):
            ...