gRPC Performance Tuning

gRPC starts fast, and careful tuning makes it faster. Measure first, change one setting at a time, and measure again. Guesses waste effort, since the slowest part is often a database query or a network hop and not gRPC itself.

The Highway Analogy

A highway moves traffic quickly until a bottleneck appears. Adding lanes elsewhere does nothing for a single narrow bridge. Performance work finds the bridge first.

Client ─▶ Network ─▶ Server code ─▶ Database
  2 ms      15 ms        8 ms          220 ms  ← bottleneck

Step 1: Measure

The ghz tool generates load against a gRPC method and reports latency percentiles.

ghz --insecure \
  --proto ./orders.proto \
  --call shop.OrderService.GetOrder \
  -d '{"order_id":"1001"}' \
  -n 10000 -c 50 \
  localhost:50051

The flag -n sets total calls and -c sets concurrent workers. Record the baseline numbers before any change.

Step 2: Profile the Server

Go includes the pprof profiler. It shows which functions use the most CPU time and memory. Run it during a load test to find hot spots.

Reuse Channels

A channel represents a long lived connection. Creating one per call repeats the TCP and TLS handshakes and destroys performance. Create a channel once at startup and share it across the program.

Slow:  each call → new channel → handshake → call → close
Fast:  one channel at startup → many calls over one connection

Use Several Channels for Heavy Traffic

One HTTP/2 connection carries many calls at once, yet servers limit concurrent streams per connection, commonly around 100. A client that sends thousands of simultaneous calls can queue behind that limit. Open a small pool of channels and spread calls across them.

Keep Messages Small

  • Send only the fields callers need
  • Use field masks for large objects
  • Use int32 and int64 instead of strings for numbers
  • Prefer repeated numeric fields, which pack tightly on the wire
  • Split huge payloads into streams

Compression

Compression shrinks large text heavy messages and costs CPU time. Enable it for big payloads on slow networks. Skip it for tiny messages, where the savings vanish.

import "google.golang.org/grpc/encoding/gzip"

conn, err := grpc.NewClient(addr,
  grpc.WithTransportCredentials(creds),
  grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)),
)

Batch or Stream Chatty Calls

A client that makes 500 tiny unary calls pays round trip costs 500 times. Combine them into one batch method, or open one stream and send all items through it.

Chatty:  GetItem(1) GetItem(2) GetItem(3) ... GetItem(500)
Better:  GetItems([1, 2, 3, ... 500])

Configure Keepalive

Keepalive pings detect dead connections and stop idle timeouts from closing healthy ones. Choose intervals of 30 seconds or more. Aggressive pings waste bandwidth, and servers may reject them.

Control Server Concurrency

Slow database calls hold a goroutine and a connection each. Limit parallel work with a semaphore or rate limiter so a traffic spike does not exhaust memory or database connections. Reject extra calls early with RESOURCE_EXHAUSTED.

Tune Garbage Collection and Allocation

Frequent large allocations trigger garbage collection pauses. Reuse buffers where practical and avoid copying byte slices needlessly. Verify improvements with profiler output.

Common Wins

ChangeTypical Benefit
Reuse one channelRemoves handshake delay per call
Batch small callsFewer round trips
Add database indexFaster queries
Field masksSmaller messages
Channel poolHigher concurrent throughput

Summary

Measure with a load tool, profile the server, and fix the real bottleneck. Reuse channels, pool them for heavy traffic, keep messages lean, batch chatty calls, and limit server concurrency. Repeat measurement after every change.