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
int32andint64instead of strings for numbers - Prefer
repeatednumeric 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
| Change | Typical Benefit |
|---|---|
| Reuse one channel | Removes handshake delay per call |
| Batch small calls | Fewer round trips |
| Add database index | Faster queries |
| Field masks | Smaller messages |
| Channel pool | Higher 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.
