This episode covers gRPC performance: measuring latency and throughput with ghz, optimizing through compression and connection reuse, handling backpressure, and composing efficient protobuf messages with packed fields and repeated fields.

gRPC is already fast by default, but "fast" isn't a number. To optimize, you must measure first — and gRPC measurement has a purpose-built tool. Episode 13 covers the performance side: measuring with ghz, then optimizing at three levels: transport (compression and connection reuse), application (backpressure), and data (protobuf message design).
The order matters: measure, identify the bottleneck, then optimize. Optimizing without measuring is just guessing.
ghz is a benchmark tool for gRPC that uses reflection — no .proto file needed:
ghz --insecure \
-n 10000 \
-c 50 \
-d '{"id":"p-001"}' \
localhost:50051 catalog.v1.CatalogService/GetProductThe -c 50 flag opens 50 concurrent workers making 10 thousand calls in total. The ghz output shows RPS, latency percentiles (p50, p90, p99), and the error count — everything you need to assess a baseline.
Focus on three metrics: RPS (requests per second) for throughput, p99 for the worst-case experience, and error rate for reliability. Save the output as a baseline before and after changes — the comparison is what proves an optimization worked.
Binary protobuf payloads are already compact, but they can get even smaller with compression. Enable it on the client:
import "google.golang.org/grpc/encoding/gzip"
res, err := client.GetProduct(ctx, &pb.ProductId{Id: "p-001"},
grpc.UseCompressor(gzip.Name))grpc.UseCompressor(gzip.Name) compresses the request with gzip. The effect is most noticeable for large payloads or over limited bandwidth — but there's a trade-off: extra CPU on both sides. For small payloads, compression can actually slow things down.
Remember the lesson from episode 10: reuse the channel, don't create a new one per request. Two additional settings that often give a performance boost:
conn, _ := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(8*1024*1024)),
grpc.WithInitialWindowSize(2*1024*1024),
grpc.WithInitialConnWindowSize(8*1024*1024),
)grpc.WithInitialWindowSize(2*1024*1024) enlarges the flow control window so high throughput isn't held back by overly aggressive backpressure. Tune it to your traffic patterns, not just copied values.
Backpressure is actually a friend, not an enemy: gRPC throttles the send rate to match the receiver's capacity. On large streams, don't call Recv sequentially in one goroutine if you want high parallelism — read from separate goroutines so flow control doesn't stall the pipeline.
There are two levels of flow control in gRPC: the connection window (applies to all streams on one connection) and the stream window (applies per stream). When throughput stalls, first check whether an undersized window is the limiter before adding threads — ghz with a larger -c often reveals this symptom faster than guessing in code.
For scalar lists, protobuf uses packed encoding by default in proto3: all elements are sent consecutively in one block, not one tag per element. That means a repeated int64 holding a thousand numbers doesn't drag a thousand tags — just the data.
message Order {
repeated int64 item_ids = 1 [packed = true];
}[packed = true] ensures packing is explicit even though it's already the default in proto3. For repeated message, encoding can't be packed — each element still carries its tag.
The data type affects the wire format size:
int32 vs sint32: for numbers that can be negative, sint32 uses ZigZag encoding and is smaller.int32 vs int64: an int32 with small values only needs 1-2 bytes.double when int32 is enough — excess precision and size aren't free.The rule of thumb from these comparisons: first measure the actual payload with grpcurl -v to see the message size, then decide which type is worth simplifying. Optimization without measurement data almost always leads to adjustments that have no impact.
Split large messages into several calls or use streaming. Two rules of thumb: don't send a user's entire history in one response, and don't put rarely used fields in the hot path. A lean message design keeps unary calls light.
Finally, don't forget to test every optimization with the exact same benchmark before and after. Changes like compression or window size can interact with each other — consistent ghz baseline testing is the only way to know which combination actually delivers results.
Key takeaways:
ghz — RPS, p99, and error rate — before changing anything.In episode 14 next, we cover gRPC observability, tracing, and monitoring — metrics with Prometheus and OpenTelemetry, distributed tracing on RPCs, span context propagation, and debugging with grpcurl and ghz on dashboards. The measured performance now needs to be monitored continuously in production.