gRPC in Production

Running gRPC in production means caring about observability, performance, schema evolution, and deployment safety. This topic covers the practical patterns that experienced teams use to operate gRPC services reliably at scale.

The Production Readiness Checklist

┌─────────────────────────────────────────────────┬──────┐
│ Item                                            │ Done │
├─────────────────────────────────────────────────┼──────┤
│ TLS enabled on all connections                  │  [ ] │
│ Authentication interceptor in place             │  [ ] │
│ Deadlines set on every outgoing call            │  [ ] │
│ Health check service registered                 │  [ ] │
│ Kubernetes liveness and readiness probes set    │  [ ] │
│ Graceful shutdown implemented                   │  [ ] │
│ Panic recovery interceptor in place             │  [ ] │
│ Structured logging per call                     │  [ ] │
│ Distributed tracing configured                  │  [ ] │
│ Metrics exported (Prometheus)                   │  [ ] │
│ Rate limiting configured                        │  [ ] │
│ Max message size limits set                     │  [ ] │
│ Reflection disabled in production               │  [ ] │
│ Proto breaking change detection in CI           │  [ ] │
└─────────────────────────────────────────────────┴──────┘

Observability — The Three Pillars

Observability means being able to understand what your system is doing from the outside. The three pillars are logs, metrics, and traces.

┌────────────┬──────────────────────────────────────────────────────┐
│ Pillar     │ What It Tells You                                    │
├────────────┼──────────────────────────────────────────────────────┤
│ Logs       │ What happened on a specific call (request, response, │
│            │ error message, call duration, user ID)               │
├────────────┼──────────────────────────────────────────────────────┤
│ Metrics    │ Aggregated counts and durations across all calls     │
│            │ (requests/sec, error rate, p99 latency)              │
├────────────┼──────────────────────────────────────────────────────┤
│ Traces     │ The journey of one request across multiple services  │
│            │ (A called B called C — which step was slow?)         │
└────────────┴──────────────────────────────────────────────────────┘

Metrics with Prometheus

The go-grpc-prometheus library instruments your gRPC server automatically. It exports standard metrics that Prometheus scrapes and Grafana displays.

import (
  grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
  "github.com/prometheus/client_golang/prometheus/promhttp"
  "net/http"
)

func main() {
  grpcServer := grpc.NewServer(
    grpc.ChainUnaryInterceptor(
      grpc_prometheus.UnaryServerInterceptor,
    ),
    grpc.ChainStreamInterceptor(
      grpc_prometheus.StreamServerInterceptor,
    ),
  )

  pb.RegisterOrderServiceServer(grpcServer, &orderServer{})

  // Enable histogram metrics for latency percentiles
  grpc_prometheus.EnableHandlingTimeHistogram()
  grpc_prometheus.Register(grpcServer)

  // Expose Prometheus metrics on a separate HTTP port
  go func() {
    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":9090", nil)
  }()

  lis, _ := net.Listen("tcp", ":50051")
  grpcServer.Serve(lis)
}

Metrics exposed automatically:
  grpc_server_started_total          — call count by method and type
  grpc_server_handled_total          — completed calls by status code
  grpc_server_handling_seconds       — latency histogram (p50, p90, p99)
  grpc_server_msg_received_total     — messages received (streaming)
  grpc_server_msg_sent_total         — messages sent (streaming)

Distributed Tracing with OpenTelemetry

OpenTelemetry is the standard for distributed tracing. It attaches a trace ID to every request. As the request travels from service to service, each hop adds a span. You can see the full call chain in tools like Jaeger or Zipkin.

Trace for one user request:
  ┌─────────────────────────────────────────────────────────────────┐
  │  Trace ID: abc-123                                              │
  │                                                                 │
  │  [API Gateway]  0ms ──────────────────────────────────── 220ms  │
  │    [OrderService]  5ms ──────────────────────────── 215ms       │
  │      [InventoryService]  8ms ──────────── 80ms                  │
  │        [DB query]  10ms ──── 30ms                               │
  │      [PaymentService]  85ms ───────────────────── 210ms         │
  │        [External API]  90ms ──────────────────── 205ms ← SLOW   │
  └─────────────────────────────────────────────────────────────────┘

The trace immediately shows that the external payment API caused 120ms of the total 220ms.

Setup in Go:
  import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "google.golang.org/grpc"
  )

  grpcServer := grpc.NewServer(
    grpc.StatsHandler(otelgrpc.NewServerHandler()),
  )

  conn, _ := grpc.NewClient("server:443",
    grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
    grpc.WithTransportCredentials(creds),
  )

Performance Tuning

Message Compression

Large payloads benefit from compression. gRPC supports gzip and snappy.
Enable per call or per channel:

  // Client — compress all outgoing calls
  conn, _ := grpc.NewClient("server:443",
    grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)),
    grpc.WithTransportCredentials(creds),
  )

  // Server — automatically decompresses what client sends
  // No extra config needed; gRPC detects grpc-encoding header

Compression sweet spot:
  • Useful for large messages (>1KB of text/JSON nested in proto)
  • Not useful for small messages — CPU cost exceeds savings
  • Not useful for already-compressed data (images, videos)

Maximum Message Size

Default max message size is 4MB. Change it explicitly:

  // Server — increase to 10MB
  grpcServer := grpc.NewServer(
    grpc.MaxRecvMsgSize(10 * 1024 * 1024),
    grpc.MaxSendMsgSize(10 * 1024 * 1024),
  )

  // Client — increase to 10MB
  conn, _ := grpc.NewClient("server:443",
    grpc.WithDefaultCallOptions(
      grpc.MaxCallRecvMsgSize(10 * 1024 * 1024),
      grpc.MaxCallSendMsgSize(10 * 1024 * 1024),
    ),
    grpc.WithTransportCredentials(creds),
  )

Better practice for very large data:
  Use server streaming or client streaming to send data in chunks
  instead of raising the limit. This keeps memory usage predictable.

Keepalive

Long-lived connections go idle. Firewalls and NAT gateways silently drop
idle TCP connections after minutes of inactivity. Keepalive pings detect
and recover from these silent drops.

Server keepalive config:
  import "google.golang.org/grpc/keepalive"

  grpcServer := grpc.NewServer(
    grpc.KeepaliveParams(keepalive.ServerParameters{
      MaxConnectionIdle:     15 * time.Minute, // close idle connections after 15min
      MaxConnectionAge:      30 * time.Minute, // max age per connection
      MaxConnectionAgeGrace: 5  * time.Second, // graceful close window
      Time:                  5  * time.Second, // ping client every 5s
      Timeout:               1  * time.Second, // close if no pong in 1s
    }),
    grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
      MinTime:             5 * time.Second, // min time between client pings
      PermitWithoutStream: true,
    }),
  )

Client keepalive:
  conn, _ := grpc.NewClient("server:443",
    grpc.WithKeepaliveParams(keepalive.ClientParameters{
      Time:                10 * time.Second,
      Timeout:             5  * time.Second,
      PermitWithoutStream: true,
    }),
    grpc.WithTransportCredentials(creds),
  )

Schema Evolution in Production

Proto schema changes must follow safe rules so existing clients and servers keep working.

Safe changes (backward compatible — deploy anytime):
  ✔ Add a new field with a new field number
  ✔ Add a new method to a service
  ✔ Add a new value to an enum (except as default=0)
  ✔ Change a field name (field number stays the same)

Unsafe changes (breaking — require coordinated rollout):
  ✘ Remove a field or reuse its number
  ✘ Change a field's type
  ✘ Remove a service method
  ✘ Change a method's request or response type

Breaking change detection in CI with Buf:
  # Install Buf CLI
  brew install bufbuild/buf/buf

  # buf.yaml — configure the registry for comparison
  version: v1
  breaking:
    use:
      - FILE

  # Run in CI to block merges that break the schema
  buf breaking --against .git#branch=main

Deployment Strategy

Safe rollout sequence for a breaking proto change:

Phase 1: Deploy new server that supports BOTH old and new schema
  Old clients → send old messages → new server handles them (backward compat)
  Monitor for errors, verify old clients still work

Phase 2: Deploy new clients that send new schema
  New clients → send new messages → new server handles them
  Monitor for errors

Phase 3: (Optional) Remove old schema support from server
  Only when all clients have been updated

This is the "expand and contract" pattern — also known as the
"parallel change" or "strangler fig" pattern.

gRPC-Gateway — Exposing REST and gRPC Together

grpc-gateway generates an HTTP/JSON reverse proxy from proto annotations.
This lets you serve both gRPC and REST from one Go process.

Proto annotation:
  import "google/api/annotations.proto";

  service UserService {
    rpc GetUser (GetUserRequest) returns (User) {
      option (google.api.http) = {
        get: "/v1/users/{user_id}"   ← REST route generated automatically
      };
    }
  }

Architecture:
  REST Client ──HTTP/JSON──► grpc-gateway ──gRPC──► gRPC Server
  gRPC Client ──gRPC──────────────────────────────► gRPC Server

Both client types can reach the same server code.

Load Testing with ghz

ghz is a gRPC load testing tool (like Apache ab or wrk for REST):

  # 1000 requests, 50 concurrent workers
  ghz --insecure \
    --proto order.proto \
    --call order.OrderService.GetOrder \
    -d '{"order_id": 1}' \
    -n 1000 -c 50 \
    localhost:50051

Output:
  Summary:
    Count:       1000
    Total:       2.34s
    Slowest:     85.3ms
    Fastest:     2.1ms
    Average:     14.8ms
    Requests/sec: 427.4

  Response time histogram:
    2.1  [50]   |████
    15.0 [700]  |████████████████████████████████████████████████
    30.0 [200]  |████████████
    85.3 [50]   |████

Summary

Production gRPC requires TLS, authentication, deadlines, health checks, and graceful shutdown as a baseline. Add observability with Prometheus metrics, structured logging per call, and OpenTelemetry traces to see across service boundaries. Tune performance with compression, keepalive pings, and appropriate message size limits. Evolve proto schemas safely using the expand-and-contract pattern, and catch breaking changes in CI with the Buf CLI. Use grpc-gateway when external clients need a REST interface alongside the gRPC API.

Leave a Comment

Your email address will not be published. Required fields are marked *