gRPC load testing: tools, setup and what to watch
6 min read
gRPC load testing needs a tool that speaks HTTP/2 and Protocol Buffers: k6 and ghz do it out of the box, JMeter needs a plugin, Gatling’s gRPC support is capped in its free edition, and Locust needs you to wrap a gRPC client yourself. Count results by gRPC status, not HTTP status, because failed calls still arrive as HTTP 200, and check that the load reaches every backend.
No gRPC test was run for this article. Tool facts come from each project’s documentation and repository, checked on 16 October 2026; LoadFocus facts come from its code.
| Tool | gRPC support | Loads the API from | Rate control |
|---|---|---|---|
| k6 | Built in (k6/net/grpc), unary calls and streams | .proto files or server reflection | Arrival-rate executors |
| ghz | Purpose-built gRPC CLI | .proto, protoset or server reflection | --rps and load schedules |
| JMeter | Third-party plugin (jmeter-grpc-request) | A folder of .proto files | Thread groups and timers |
| Gatling | Separate gRPC component; free edition capped at 5 users, 5 minutes | Code generated from .proto with protoc | Injection profiles |
| Locust | No built-in client; wrap one and report calls | Your Python gRPC client | Wait times, constant_throughput |
Why does gRPC need a different load test from REST?
gRPC runs over HTTP/2, sends binary Protocol Buffers instead of JSON, puts the call’s result in a trailer rather than the HTTP status, and multiplexes many calls on one connection. Each of those changes how you script the test or read its results.
The gRPC load balancing post lists the HTTP/2 traits that matter here: a binary protocol, many requests multiplexed on one connection and header compression, with strongly typed Protobuf messages on top (checked on 16 October 2026). A tool that only sends JSON over HTTP/1.1 cannot call a gRPC method, so check protocol support first.
Which tool should you use?
Use k6 if you want scripted scenarios, checks and thresholds in code; use ghz for a quick benchmark of one method from the command line. Reach for JMeter, Gatling or Locust when your team already runs everything else in them.
- k6. The k6/net/grpc module loads Protobuf definitions, connects, makes unary calls with
invokeand opens streams. It reportsgrpc_req_durationand stream message counts in its metrics. - ghz. A gRPC-only benchmarking CLI. Its options include
--rps,--concurrency,--totaland--duration, and if you pass neither--protonor--protosetit tries server reflection. The latest release is v0.121.0 (checked on 16 October 2026). - JMeter. Core JMeter has no gRPC sampler; the jmeter-grpc-request plugin adds one that reads a folder of
.protofiles, with no code generation (latest tag v1.2.6, checked on 16 October 2026). - Gatling. The Gatling README puts gRPC, MQTT and GraphQL under the Gatling Enterprise Component License, capped at 5 concurrent users and 5-minute tests in Community Edition (checked on 16 October 2026). Its gRPC setup guide generates code from your
.protofiles with protoc. - Locust. Its docs on testing other systems include a gRPC example: you wrap a gRPC client so each call is reported to Locust, and
constant_throughputpaces a task to at most a set number of runs per second.
Choose ghz instead of k6 when you only need to know how one method behaves at a given rate and do not want to write a script. Choose Locust instead when your team writes Python and the test needs logic that is easier there.
How do you write a gRPC load test in k6?
Load the .proto file in the init code, then connect, invoke the method and check the gRPC status in each iteration. The structure follows the k6 documentation’s own example.
import grpc from 'k6/net/grpc';
import { check } from 'k6';
const client = new grpc.Client();
client.load(['protos'], 'hello.proto');
export const options = { vus: 20, duration: '1m' };
export default () => {
client.connect('grpc.example.com:443');
const res = client.invoke('hello.Greeter/SayHello', {
name: 'load test',
});
check(res, {
'status is OK': (r) => r && r.status === grpc.StatusOK,
});
client.close();
};
The invoke page uses the same load, connect, invoke, check and close sequence. connect uses TLS unless you pass plaintext: true, and with reflect: true it asks the server for its API instead of reading a .proto file (connect parameters). To hold a request rate rather than a number of users, run the function in an arrival-rate scenario.
How do you run a quick benchmark with ghz?
Point ghz at the method, give it a request body and a rate or concurrency, and it prints a latency histogram, percentiles and a count for each status code. It suits a first look at one method.
ghz --insecure --proto ./hello.proto \
--call hello.Greeter.SayHello \
-d '{"name":"load test"}' \
--rps 200 -c 50 -z 1m \
localhost:50051
--rps sets the target request rate, shared among the workers, -c sets how many workers there are, and -z sets the duration. Drop --proto to let ghz use server reflection, if the server enables it. The ghz output page shows a sample report.
What should you measure in a gRPC load test?
Measure call latency percentiles, throughput and the count of each gRPC status code. An HTTP-level success rate can read 100% while calls fail, because the result travels in the grpc-status trailer.
The gRPC HTTP/2 protocol spec sets the HTTP status to :status 200 and carries the call’s result in trailers as grpc-status and grpc-message. The status code list has the codes that matter most under load:
DEADLINE_EXCEEDED(4): the deadline expired before the call completed.RESOURCE_EXHAUSTED(8): a quota or resource ran out.INTERNAL(13): an internal invariant broke, a code reserved for serious errors.UNAVAILABLE(14): usually transient; retrying with backoff can clear it, though not always safely for non-idempotent calls.
Count each one per load step. Deadline errors can climb while the calls that succeed still look fast, because the slow calls are cut off at the deadline instead of being recorded as slow. Our guide to load test pass/fail criteria shows how to turn those counts into thresholds.
Why does all the load land on one server?
Because gRPC multiplexes many calls on one long-lived connection, a load balancer that works per connection sends every call on it to the same backend. A load generator with few connections can then saturate one server while the others sit idle.
The gRPC load balancing post explains the mechanism. A transport-level (L3/L4) balancer terminates the TCP connection and opens another to one chosen backend, copying the HTTP/2 and gRPC frames between them; an application-level (L7) balancer parses HTTP/2 and can spread the streams of one client connection across backends. If your production clients go through an L7 balancer or balance on the client side, test through the same path, and check per-backend CPU and request counts during the run.
Can you run gRPC load tests on LoadFocus?
Not in a way its reports would show. LoadFocus runs the stock k6 v0.57.0 release, which includes the gRPC module, but its k6 agent forwards only HTTP request samples to the results, so gRPC calls would generate load without appearing in its charts or error counts.
Two more limits apply. The k6 upload accepts .js, .ts, .csv, .txt, .json, .env and .properties files, not .proto files, so a script would have to connect with reflect: true. And the JMeter route is closed: LoadFocus’s JMeter agents ship JMeter 5.6.2 with plugins that do not include a gRPC sampler, and the JMeter upload does not accept plugin JAR files. For gRPC, run k6 or ghz on your own machines, and use LoadFocus’s k6 scripts for the HTTP side of the same system, where it generates load from 25 regions.
FAQ
Can you load test gRPC streaming calls?
Yes, with k6 and ghz. The k6 gRPC module has a Stream class with write, end and event handlers, and ghz has options for streaming intervals and durations. Measure messages sent and received as well as call latency.
Do you need the .proto file to load test a gRPC service?
Not if the server enables reflection. k6 can connect with reflect: true, and ghz falls back to reflection when you pass neither --proto nor --protoset. Without reflection you need the .proto file or a compiled protoset.
Why do my gRPC errors not show up as failed requests?
Because the HTTP status of a gRPC call is 200 even when the call fails. The result is in the grpc-status trailer, so check that value in your script and count each code separately.
Related reading
- WebSocket load testing tools: six options compared
- How to load test a GraphQL API: a practical guide
- Gatling vs k6: JVM simulations or a single Go binary?
- HTTP benchmarking tools: wrk, hey, oha, ab and more
The technical claims in this article have not been independently reviewed.