Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
06 Sep 2025 · 6 min read ·Article 90 / 110
Go

90. Case Study: Simulating gRPC Load Testing

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

90. Case Study: Simulating gRPC Load Testing

These days, services built on the gRPC protocol are increasingly popular across various technology companies thanks to their efficiency in handling communication between microservices. Built on top of HTTP/2, gRPC offers excellent performance and a highly efficient serialization protocol (Protocol Buffers), making it well suited for large-scale systems. However, before rolling gRPC out to production, understanding how the service performs under heavy load is an essential step. In this article, I’ll walk through a case study of load testing a gRPC service, complete with sample code, a simulation, and an analysis of the results.


Why Do You Need to Load Test gRPC?

Load testing aims to simulate large volumes of traffic against a system so we can identify bottlenecks, evaluate throughput and response time, and validate error-handling scenarios. There are a few reasons we can’t simply assume gRPC performance is always “blazing fast”:

  • gRPC still consumes network and system resources, and bottlenecks can occur in deserialization, the database, or concurrency.
  • Testing with light load doesn’t represent real-world production scenarios.
  • There are resource limits to consider, such as memory, connection pools, or CPU usage.

For these reasons, load testing simulations are key to making sure a gRPC service is ready for production deployment.


Case Study: A Simple User Service

Suppose we have a User Service that exposes a gRPC endpoint for a GetUser operation. Here is an example protocol buffer definition and a simple server implementation:

user.proto

proto
 1syntax = "proto3";
 2
 3package user;
 4
 5service UserService {
 6  rpc GetUser (GetUserRequest) returns (GetUserResponse);
 7}
 8
 9message GetUserRequest {
10  string user_id = 1;
11}
12
13message GetUserResponse {
14  string user_id = 1;
15  string name = 2;
16  string email = 3;
17}

A Simple Implementation (Go)

go
 1type userServer struct{
 2  user.UnimplementedUserServiceServer
 3}
 4
 5func (s *userServer) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.GetUserResponse, error) {
 6  // Simulate reading data
 7  return &user.GetUserResponse{
 8    UserId: req.GetUserId(),
 9    Name: "Jane Doe",
10    Email: "janedoe@example.com",
11  }, nil
12}

In this example, GetUser simply returns some basic data — but that’s enough to test the request-response performance of gRPC.


Defining the Load Testing Parameters

Before running a load test, define the following parameters:

  • Number of concurrent users: How many simultaneous requests will you simulate? (e.g., 200, 500)
  • RPS (Requests per Second): What request rate do you want to reach?
  • Test duration: How long will the simulation run (e.g., 1 minute, 5 minutes)?
  • Threshold: The expected response-time SLA/SLI (e.g., 95% of requests < 200ms).

Tools for gRPC Load Testing

Some open source tools for load testing gRPC include:

  • ghz (link ) — a powerful and easy-to-use CLI tool.
  • k6 with the gRPC extension .
  • Locust with a plugin, or a custom script.

In this case study, we’ll use ghz, because:

  • It’s easy to use as a CLI
  • It supports a variety of output report formats
  • It can be customized via scripts and integrated into CI/CD

Simulating a Load Test with ghz

Installation (via the CLI)

bash
1go install github.com/bojand/ghz/cmd/ghz@latest

Running the Simulation

Assuming the service is running on localhost:50051, the following command runs the load test:

bash
1ghz --insecure \
2    --proto user.proto \
3    --call user.UserService.GetUser \
4    -d '{"user_id": "123"}' \
5    -c 200 -n 10000 \
6    localhost:50051
  • -c 200 indicates 200 concurrent connections.
  • -n 10000 indicates a total of 10,000 requests will be sent.

Sample Output & Analysis

The simulation ran for about 1 minute, with the following results:

MetricValue
Count10000
Slowest157ms
Fastest12ms
Average42.5ms
95th percentile74ms
Requests/sec350
Errors0

These results indicate the service can handle 350 RPS with most requests served at a latency below 100ms.


Visualizing the Flow with a Diagram (mermaid)

Let’s look at the load testing process as a diagram.

MERMAID
sequenceDiagram
    participant Client as ghz
    participant Proxy as Load Balancer
    participant Service as gRPC User Service

    Client->>Proxy: Kirim request GetUser (200 concurrent)
    Proxy->>Service: Forward ke gRPC instance (balancing)
    Service->>Proxy: Kembalikan response
    Proxy->>Client: Terima response (dan log latency)

Experiment: Increasing the Load

Now, let’s bump the concurrency up to 500 and send 20,000 requests:

bash
1ghz --insecure \
2    --proto user.proto \
3    --call user.UserService.GetUser \
4    -d '{"user_id": "123"}' \
5    -c 500 -n 20000 \
6    localhost:50051

Sample output

MetricValue
Count20000
Slowest350ms
Fastest30ms
Average110ms
95th percentile240ms
Requests/sec450
Errors87

As you can see, at higher concurrency the error count rises, response time increases quite significantly, and some requests fail due to timeouts or resource limits.


Bottlenecks and Optimization

Based on the results above, a few critical questions come to mind:

  • Why do errors occur at high concurrency?
    The common answer: the thread pool may be exhausted, garbage collection, memory limits, or some other resource constraint.
  • How do you address it?
    • Horizontal scaling: deploy the service in a distributed fashion.
    • Optimize resource limits: enlarge the thread pool, tune the GC/runtime.
    • Check and optimize downstream dependencies, such as the database.

Monitoring & Reporting

For load testing results to be useful, they must be recorded and visualized. ghz supports exporting to JSON/HTML, so you can produce a chart like the following.

json
1{
2  "summary": {
3    "total": 20000,
4    "average": 110,
5    "q95": 240,
6    "errors": 87,
7    "rps": 450
8  }
9}

Cross-check your load test results against your production Grafana/Prometheus dashboards to validate the findings.


Practical Tips When Running a Load Test Simulation

  1. Run it in a separate environment
    So you don’t disrupt real user traffic.
  2. Replicate the production topology
    Simulate the real components: reverse proxy, load balancer, service discovery.
  3. Account for other effects
    For example, network security, rate limiting, and observability tooling.
  4. Automate it as part of CI/CD
    Run load tests regularly (e.g., as part of the nightly build) to catch service regressions.

Conclusion

Load testing a gRPC service isn’t just a formality — it’s a crucial step to ensure the reliability and scalability of our service. With a tool like ghz, it’s easy to set up a simulation and obtain a complete set of metric reports. By learning from the simulation results, engineers can optimize the service and avoid unexpected surprises when production traffic surges. The more thorough your load testing, the healthier your microservices ecosystem will be.

Do you have an interesting experience with load testing gRPC or other microservices? I’d love to hear your discussions and insights in the comments!

Related Articles

💬 Comments