Skip to content

gRPC & Protocol Buffers - Primer

Why This Matters

REST APIs serialize data as JSON over HTTP/1.1 — human-readable, broadly supported, and slow. In a microservice architecture with hundreds of services making thousands of internal calls per second, JSON parsing and HTTP/1.1 connection overhead become measurable costs. gRPC replaces this with Protocol Buffers (binary serialization, 3-10x smaller than JSON) over HTTP/2 (multiplexed streams, header compression, bidirectional communication). Google built gRPC for exactly this problem: their internal services make billions of RPCs per second.

For DevOps teams, gRPC changes the operational surface. Services communicate over HTTP/2 on a single TCP connection with multiplexed streams. Load balancing requires L7 awareness (L4 load balancers cannot distribute streams within a single connection). Health checking uses a standardized protocol (grpc.health.v1). Debugging requires tools like grpcurl instead of curl. If you run Kubernetes, most of the control plane (etcd, kubelet, API server) uses gRPC internally.

Core Concepts

1. Protocol Buffers (Protobuf)

Protobuf is a language-neutral, platform-neutral serialization format. You define message types in .proto files and generate code for your target language.

// user.proto
syntax = "proto3";

package userservice;

option go_package = "github.com/example/userservice/pb";

message User {
  string id = 1;           // field number, NOT value
  string email = 2;
  string name = 3;
  repeated string roles = 4;  // repeated = list
  google.protobuf.Timestamp created_at = 5;
}

message GetUserRequest {
  string id = 1;
}

message GetUserResponse {
  User user = 1;
}

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
  rpc ListUsers (ListUsersRequest) returns (stream User);  // server streaming
  rpc UpdateUsers (stream UpdateUserRequest) returns (UpdateUsersResponse);  // client streaming
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);  // bidirectional
}

Compile protobuf to language-specific code:

# Install protoc compiler
apt install -y protobuf-compiler

# Install Go gRPC plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# Generate Go code
protoc --go_out=. --go-grpc_out=. proto/user.proto

# Generate Python code
pip install grpcio-tools
python -m grpc_tools.protoc -I proto/ --python_out=. --grpc_python_out=. proto/user.proto

Field number rules: - Field numbers 1-15 use 1 byte in the wire format (use for frequently set fields) - Field numbers 16-2047 use 2 bytes - Never reuse a field number after removing a field — use reserved - reserved 6, 15, 9 to 11; prevents future reuse

2. gRPC Communication Patterns

Pattern Description Use Case
Unary One request, one response Simple CRUD
Server streaming One request, stream of responses Log tailing, large result sets
Client streaming Stream of requests, one response File upload, batch writes
Bidirectional streaming Both sides stream independently Chat, real-time sync

All four patterns multiplex over a single HTTP/2 connection. Each RPC is an independent stream with its own flow control.

3. gRPC Health Checking Protocol

gRPC defines a standard health checking protocol (grpc.health.v1) that Kubernetes, load balancers, and service meshes use:

// Standard health check service (built into gRPC libraries)
service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

Kubernetes gRPC health check (v1.24+):

apiVersion: v1
kind: Pod
metadata:
  name: user-service
spec:
  containers:
    - name: app
      image: example/user-service:v1
      ports:
        - containerPort: 50051
      livenessProbe:
        grpc:
          port: 50051
        initialDelaySeconds: 10
        periodSeconds: 5
      readinessProbe:
        grpc:
          port: 50051
          service: "user-service"  # optional: check specific service
        initialDelaySeconds: 5
        periodSeconds: 3

4. Debugging with grpcurl

grpcurl is curl for gRPC. It requires the server to support reflection or you to provide proto files.

# Install
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest

# List available services (requires server reflection enabled)
grpcurl -plaintext localhost:50051 list

# Describe a service
grpcurl -plaintext localhost:50051 describe userservice.UserService

# Call a method
grpcurl -plaintext -d '{"id": "user-123"}' \
  localhost:50051 userservice.UserService/GetUser

# Call with TLS (default — no -plaintext flag)
grpcurl -cacert ca.pem -cert client.pem -key client-key.pem \
  api.example.com:443 userservice.UserService/GetUser

# Use proto file instead of reflection
grpcurl -plaintext -import-path ./proto -proto user.proto \
  -d '{"id": "user-123"}' localhost:50051 userservice.UserService/GetUser

# Stream output
grpcurl -plaintext -d '{"filter": "active"}' \
  localhost:50051 userservice.UserService/ListUsers

Enable server reflection (required for grpcurl discovery):

// Go
import "google.golang.org/grpc/reflection"
s := grpc.NewServer()
reflection.Register(s)

# Python
from grpc_reflection.v1alpha import reflection
reflection.enable_server_reflection(SERVICE_NAMES, server)

5. Load Balancing

gRPC over HTTP/2 multiplexes all RPCs over a single TCP connection. An L4 load balancer (TCP) sees one connection and sends all traffic to one backend. This is the single most common gRPC operational mistake.

Solutions:

Approach How it works When to use
L7 load balancer Envoy, Nginx, Traefik inspect HTTP/2 frames External-facing, sidecar-free
Client-side LB Client resolves multiple backends, round-robins Internal services, high-performance
Service mesh sidecar Envoy/Linkerd proxy handles LB Kubernetes with Istio/Linkerd
Lookaside LB External LB service (gRPC-LB protocol) Large-scale, Google-style

Envoy gRPC load balancing config:

# envoy.yaml
static_resources:
  listeners:
    - address:
        socket_address: { address: 0.0.0.0, port_value: 8080 }
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                codec_type: AUTO
                stat_prefix: grpc
                route_config:
                  virtual_hosts:
                    - name: grpc_service
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route: { cluster: grpc_backend }
  clusters:
    - name: grpc_backend
      type: STRICT_DNS
      lb_policy: ROUND_ROBIN
      typed_extension_protocol_options:
        envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
          "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
          explicit_http_config:
            http2_protocol_options: {}
      load_assignment:
        cluster_name: grpc_backend
        endpoints:
          - lb_endpoints:
              - endpoint: { address: { socket_address: { address: backend1, port_value: 50051 }}}
              - endpoint: { address: { socket_address: { address: backend2, port_value: 50051 }}}

6. Interceptors (Middleware)

gRPC interceptors are the equivalent of HTTP middleware — logging, auth, metrics, tracing:

// Go unary server interceptor for logging
func loggingInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("method=%s duration=%s error=%v", info.FullMethod, time.Since(start), err)
    return resp, err
}

s := grpc.NewServer(grpc.UnaryInterceptor(loggingInterceptor))

7. Deadlines and Timeouts

gRPC propagates deadlines across service boundaries. If Service A calls Service B with a 5s deadline, and Service B calls Service C, Service C inherits the remaining deadline automatically.

// Set a 5 second deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "123"})
if err != nil {
    st, _ := status.FromError(err)
    if st.Code() == codes.DeadlineExceeded {
        // Handle timeout
    }
}

Always set deadlines. A gRPC call without a deadline can hang forever, leaking goroutines and connections.

Quick Reference

# grpcurl — list, describe, call
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext localhost:50051 describe myservice.MyService
grpcurl -plaintext -d '{}' localhost:50051 myservice.MyService/MyMethod

# grpc_health_probe — standalone health check binary
grpc_health_probe -addr=localhost:50051
grpc_health_probe -addr=localhost:50051 -service=my-service

# protoc — compile proto files
protoc --go_out=. --go-grpc_out=. proto/*.proto

# ghz — gRPC load testing
ghz --insecure --call userservice.UserService/GetUser \
  -d '{"id":"user-1"}' -n 10000 -c 50 localhost:50051

# Check HTTP/2 connectivity
curl -v --http2 https://api.example.com/

Common gRPC status codes: | Code | Name | Meaning | |------|------|---------| | 0 | OK | Success | | 1 | CANCELLED | Client cancelled | | 3 | INVALID_ARGUMENT | Bad request | | 4 | DEADLINE_EXCEEDED | Timeout | | 5 | NOT_FOUND | Resource missing | | 7 | PERMISSION_DENIED | Auth failed | | 8 | RESOURCE_EXHAUSTED | Rate limited | | 13 | INTERNAL | Server bug | | 14 | UNAVAILABLE | Transient failure (retry) |


Wiki Navigation

Prerequisites

  • HTTP Protocol (Topic Pack, L0) — HTTP Protocol
  • HTTP Protocol Flashcards (CLI) (flashcard_deck, L1) — HTTP Protocol