Locust, the open-source load testing tool, can be surprisingly effective at stressing gRPC services, especially when dealing with Protobuf.
Here’s a gRPC service definition and a Locust test script to demonstrate:
greeter.proto
syntax = "proto3";
package greeter;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
locustfile.py
from locust import HttpUser, task, between
import grpc
import time
# Import generated protobuf code
from generated.greeter_pb2 import HelloRequest
from generated.greeter_pb2_grpc import GreeterStub
class GrpcUser(HttpUser):
wait_time = between(1, 5)
host = "localhost:50051" # Replace with your gRPC server address
def on_start(self):
# Establish a gRPC channel. Locust's HttpUser uses HTTP,
# so we need to manage the gRPC channel separately.
# We'll create a new channel for each user instance.
self.channel = grpc.insecure_channel(self.host)
self.stub = GreeterStub(self.channel)
self.start_time = time.time()
def on_stop(self):
# Close the channel when the user stops
self.channel.close()
@task
def say_hello(self):
request = HelloRequest(name="Locust")
try:
# Time the gRPC call specifically
call_start_time = time.time()
response = self.stub.SayHello(request)
call_end_time = time.time()
response_time = int((call_end_time - call_start_time) * 1000) # milliseconds
# Report the response time to Locust
self.environment.events.request.fire(
request_type="gRPC",
name="SayHello",
response_time=response_time,
response_length=len(response.message.encode('utf-8')), # Approximate response size
context={},
exception=None,
)
print(f"Received: {response.message}")
except grpc.RpcError as e:
call_end_time = time.time()
response_time = int((call_end_time - call_start_time) * 1000)
self.environment.events.request.fire(
request_type="gRPC",
name="SayHello",
response_time=response_time,
response_length=0,
context={},
exception=e,
)
print(f"gRPC Error: {e.code()} - {e.details()}")
# To run this:
# 1. Ensure you have a gRPC server running on localhost:50051 that implements the Greeter service.
# 2. Install necessary libraries: pip install locust grpcio grpcio-tools
# 3. Generate Python protobuf code:
# python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. greeter.proto
# (This creates 'greeter_pb2.py' and 'greeter_pb2_grpc.py'. You might need to put these in a 'generated' subdirectory
# and adjust the import statement in locustfile.py accordingly.)
# 4. Run Locust: locust -f locustfile.py
The surprising truth about load testing gRPC with Locust is that Locust’s HttpUser is fundamentally designed for HTTP, but you can hijack its event reporting to capture gRPC metrics by managing the gRPC channel and stub directly within the user’s lifecycle.
In the example above, we define a GrpcUser that inherits from HttpUser. The on_start method is crucial here. Instead of relying on Locust’s built-in HTTP client, we establish a raw grpc.insecure_channel to our gRPC server and create a GreeterStub. This allows us to make direct gRPC calls. The on_stop method ensures the channel is properly closed.
The @task method, say_hello, constructs a HelloRequest using the generated Protobuf classes. The actual gRPC call is made using self.stub.SayHello(request). The key to integrating with Locust’s reporting is the manual timing of the call and the subsequent firing of self.environment.events.request.fire. We report the request_type as "gRPC", the name as the RPC method, and crucially, the response_time in milliseconds. We also report response_length and any exception that occurs. This allows Locust’s web UI to display gRPC requests as first-class citizens.
To make this work, you need a running gRPC server implementing the Greeter service. You’ll also need to install locust, grpcio, and grpcio-tools. The grpc_tools.protoc command generates the Python stubs and message classes from your .proto file. The locustfile.py then imports these generated files.
The mental model here is that Locust provides the user simulation and reporting framework. We are essentially using it as a sophisticated scheduler and metric collector, while performing the actual gRPC communication outside of Locust’s default HTTP client. The HttpUser base class is still useful for its wait_time and user lifecycle management.
A common pitfall is assuming Locust’s built-in client can handle gRPC. It cannot. You must manage the grpc.Channel and grpc.Stub yourself within the HttpUser’s methods. Another common mistake is not manually firing the request.fire event, which means your gRPC calls won’t appear in Locust’s statistics.
The trickiest part for many is realizing that the host attribute on HttpUser is still used, but only by Locust for its internal HTTP health checks if enabled. For your gRPC calls, you explicitly use the host variable when creating the grpc.insecure_channel.
Once you have this basic setup, you can extend it to handle more complex gRPC scenarios, including streaming calls, by carefully managing the gRPC client-side stream iterators and reporting their completion or failure to Locust.