Skip to main content
Profiles define which RPC methods to test and their relative weights. Creating custom profiles allows you to match your real-world traffic patterns or focus on specific functionality.

Profile Basics

A profile is a Python file that contains a class inheriting from EvmUser (for EVM chains) or SolanaUser (for Solana). Profiles are located in the chainbench/profile directory, organized by blockchain.
This guide provides comprehensive instructions for creating custom profiles tailored to your testing needs.

Creating Your First Profile

Let’s create a custom profile for testing Oasis blockchain.
1

Create the profile file

Create a new file at chainbench/profile/oasis/general.py:
from chainbench.user import EvmUser

class OasisProfile(EvmUser):
    pass
2

Configure wait time

Add a wait time between requests using constant_pacing:
from chainbench.user import EvmUser
from locust import constant_pacing

class OasisProfile(EvmUser):
    wait_time = constant_pacing(1)
This sets a dynamic wait time calculated as n - response_time. If response time exceeds n, the next request is sent immediately.
3

Add tasks for RPC methods

Add methods you want to test as tasks:
from chainbench.user import EvmUser
from chainbench.util.rng import get_rng
from locust import task, constant_pacing

class OasisProfile(EvmUser):
    wait_time = constant_pacing(2)

    @task
    def get_block_by_number_task(self):
        self.make_rpc_call(
            name="get_block_by_number",
            method="eth_getBlockByNumber",
            params=self._block_params_factory(),
        )

    @task
    def get_syncing_task(self):
        self.make_rpc_call(
            name="get_syncing",
            method="eth_syncing",
        )

Test Data and Parameter Factories

EvmUser comes with EvmTestData that automatically fetches real blockchain data before tests start. This data is used to generate realistic parameters for your RPC calls.

Available Parameter Factories

  • _block_params_factory(): Returns random block number with transaction details flag
  • _transaction_by_hash_params_factory(rng): Returns random transaction hash
  • _get_balance_params_factory(rng): Returns random address and block parameter
  • _random_block_number_params_factory(rng): Returns random block number only
Use get_rng() to get a seeded random number generator that ensures consistent test data across workers and runs:
from chainbench.util.rng import get_rng

params=self._transaction_by_hash_params_factory(get_rng())

Adding Multiple Tasks

Extend your profile with additional RPC methods:
from chainbench.user import EvmUser
from chainbench.util.rng import get_rng
from locust import task, constant_pacing

class OasisProfile(EvmUser):
    wait_time = constant_pacing(2)

    @task
    def get_block_by_number_task(self):
        self.make_rpc_call(
            name="get_block_by_number",
            method="eth_getBlockByNumber",
            params=self._block_params_factory(),
        )

    @task
    def get_balance_task(self):
        self.make_rpc_call(
            name="get_balance",
            method="eth_getBalance",
            params=self._get_balance_params_factory(get_rng()),
        )

    @task
    def get_transaction_by_hash_task(self):
        self.make_rpc_call(
            name="get_transaction_by_hash",
            method="eth_getTransactionByHash",
            params=self._transaction_by_hash_params_factory(get_rng()),
        )

    @task
    def get_block_number_task(self):
        self.make_rpc_call(
            name="block_number",
            method="eth_blockNumber",
        )

Task Weights

By default, all tasks have equal weight. Use task weights to match real-world traffic patterns:
from chainbench.user import EvmUser
from locust import task, constant_pacing

class CustomProfile(EvmUser):
    wait_time = constant_pacing(1)

    @task(100)  # Called 100x more often
    def call_task(self):
        self.make_rpc_call(
            name="call",
            method="eth_call",
            params=[
                {
                    "to": "0x55d398326f99059fF775485246999027B3197955",
                    "data": "0x70a08231000000000000000000000000f977814e90da44bfa03b6295a0616a897441acec",
                },
                "latest",
            ],
        )

    @task(10)  # Called 10x as often
    def get_block_number_task(self):
        self.make_rpc_call(
            name="block_number",
            method="eth_blockNumber",
        )

    @task(1)  # Base frequency
    def get_syncing_task(self):
        self.make_rpc_call(
            name="get_syncing",
            method="eth_syncing",
        )
Task weights are relative. A task with weight 100 runs 10x more often than a task with weight 10.

Static vs Dynamic Parameters

Static Parameters

For calls with fixed parameters, pass them directly:
@task
def get_syncing_task(self):
    self.make_rpc_call(
        name="get_syncing",
        method="eth_syncing",
    )

@task
def call_specific_contract(self):
    self.make_rpc_call(
        name="call",
        method="eth_call",
        params=[
            {
                "to": "0x...",
                "data": "0x...",
            },
            "latest",
        ],
    )

Dynamic Parameters

Use parameter factories for randomized test data:
@task
def get_transaction_by_hash_task(self):
    self.make_rpc_call(
        name="get_transaction_by_hash",
        method="eth_getTransactionByHash",
        params=self._transaction_by_hash_params_factory(get_rng()),
    )

Simplified Profile Syntax

For simpler profiles, use the rpc_calls dictionary syntax:
from locust import constant_pacing
from chainbench.user import EvmUser

class EvmLightProfile(EvmUser):
    wait_time = constant_pacing(1)

    rpc_calls = {
        EvmUser.eth_get_transaction_receipt: 1,
        EvmUser.eth_block_number: 1,
        EvmUser.eth_get_balance: 1,
        EvmUser.eth_chain_id: 1,
        EvmUser.eth_get_block_by_number: 1,
        EvmUser.eth_get_transaction_by_hash: 1,
        EvmUser.web3_client_version: 1,
    }

    tasks = EvmUser.expand_tasks(rpc_calls)
This is equivalent to defining individual @task methods but more concise.

Running Your Custom Profile

Once created, run your profile with:
chainbench start --profile oasis.general --users 50 --workers 2 --test-time 1h --target https://node-url --headless --autoquit
Verify your profile is detected:
chainbench list profiles

Using Custom Profile Directories

Store profiles outside the default directory:
chainbench start --profile-dir /path/to/custom/profiles --profile my-custom-profile --users 50 --workers 2 --test-time 1h --target https://node-url --headless --autoquit
Or specify the full path directly:
chainbench start --profile-path /path/to/custom/profile.py --users 50 --workers 2 --test-time 1h --target https://node-url --headless --autoquit

Advanced: Custom Test Data

Specifying Block Ranges

Control which blocks to use for test data:
chainbench start --profile custom.profile --users 50 --workers 2 --test-time 1h --target https://node-url --start-block 1000000 --end-block 1001000 --headless --autoquit
The --use-latest-blocks flag overrides custom block ranges.

Reference Node for Data Generation

Generate test data from a different node:
chainbench start --profile custom.profile --users 50 --workers 2 --test-time 1h --target https://node-to-test --ref-url https://archive-node --headless --autoquit
This is useful when your target node has limited historical data.

Profile Organization Best Practices

Directory Structure

chainbench/profile/
├── ethereum/
│   ├── general.py
│   ├── consensus.py
│   └── subscriptions.py
├── bsc/
│   └── general.py
├── polygon/
│   └── general.py
└── custom/
    ├── my-profile.py
    └── test-profile.py

Naming Conventions

  • Use lowercase with underscores: my_profile.py
  • Class names in PascalCase: MyCustomProfile
  • Descriptive names: general.py, archive.py, heavy.py

Documentation

Add docstrings to your profiles:
"""
Custom BSC profile matching production traffic patterns.

Distribution:
- eth_call: 33%
- eth_getTransactionReceipt: 31%
- eth_getLogs: 12%
- eth_blockNumber: 9%
- Others: 15%
"""

from locust import constant_pacing
from chainbench.user import EvmUser

class BscProductionProfile(EvmUser):
    wait_time = constant_pacing(1)
    # ...

Testing Your Profile

Validate your custom profile before long test runs:
1

Verify profile loads

chainbench list profiles --profile-dir /path/to/profiles
2

Run a short test

chainbench start --profile custom.test --users 5 --workers 1 --test-time 30s --target https://test-node --size XS --headless --autoquit
3

Check results

Review the output for errors and verify all methods executed successfully
4

Scale up gradually

Increase users, workers, and test duration incrementally
Always test custom profiles with --size XS first to minimize test data generation time during development.

Build docs developers (and LLMs) love