Contents

Chapter 1

Protobuf Code Generation — Step-by-Step Instructions

This guide gives you the exact commands to generate gRPC client/server stubs from the .proto files in this template, for Python, Go, and Node.js.


Prerequisites

Install Protocol Buffer Compiler (protoc)

The protoc compiler parses .proto files and generates language-specific code. Each language also needs a gRPC plugin.

OSInstall Command
macOS (Homebrew)brew install protobuf
Ubuntu / Debiansudo apt install -y protobuf-compiler
Fedora / RHELsudo dnf install -y protobuf-compiler
Windows (Chocolatey)choco install protoc
From sourcegithub.com/protocolbuffers/protobuf/releases

Verify the installation:

bash
protoc --version
# libprotoc 25.x or later

Project Layout

After code generation, your directory should look like this:

grpc-service-templates/
├── proto/
│   ├── common.proto          # Shared messages (pagination, errors, metadata)
│   └── service.proto         # CatalogService definition
├── python/
│   ├── server.py             # Server implementation (provided)
│   ├── client.py             # Client example (provided)
│   └── gen/                  # ← Generated code goes here
│       ├── common_pb2.py
│       ├── common_pb2_grpc.py
│       ├── service_pb2.py
│       └── service_pb2_grpc.py
├── go/
│   ├── server.go             # Server implementation (provided)
│   ├── client.go             # Client example (provided)
│   ├── go.mod                # ← Create this
│   └── gen/
│       └── catalogpb/        # ← Generated code goes here
│           ├── common.pb.go
│           ├── common_grpc.pb.go
│           ├── service.pb.go
│           └── service_grpc.pb.go
└── node/
    ├── server.js             # Server implementation (provided)
    ├── client.js             # Client example (provided)
    ├── package.json          # Dependencies (provided)
    └── gen/                  # ← Generated code goes here (optional for Node)

Python Code Generation

Step 1: Install the Python gRPC tools

bash
pip install grpcio grpcio-tools grpcio-health-checking

grpcio-tools includes both the protoc compiler and the Python gRPC plugin, so you don't need a separate protoc install for Python.

Step 2: Create the output directory

bash
mkdir -p python/gen

Step 3: Generate the code

Run from the project root directory:

bash
# Generate Python protobuf + gRPC stubs for both proto files.
#
# Flags explained:
#   -I.                    — Add the project root to the proto import path
#                            (so "import proto/common.proto" in service.proto resolves)
#   --python_out=python/gen — Where to write the *_pb2.py files (message classes)
#   --grpc_python_out=python/gen — Where to write the *_pb2_grpc.py files (service stubs)

python -m grpc_tools.protoc \
    -I. \
    --python_out=python/gen \
    --grpc_python_out=python/gen \
    proto/common.proto \
    proto/service.proto

Step 4: Fix the import paths

The generated service_pb2_grpc.py will contain:

python
import proto.common_pb2 as common__pb2

This only works if you run your server from the project root with the gen directory on sys.path. For a cleaner setup, create an __init__.py:

bash
touch python/gen/__init__.py

Then in server.py and client.py, add the gen directory to the path:

python
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "gen"))

Step 5: Verify

bash
python -c "from python.gen import service_pb2, service_pb2_grpc; print('Python codegen OK')"

Go Code Generation

Step 1: Install the Go protoc plugins

bash
# The Go protobuf plugin (generates *.pb.go — message types)
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

# The Go gRPC plugin (generates *_grpc.pb.go — service stubs)
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Make sure $GOPATH/bin (or $HOME/go/bin) is in your $PATH:

bash
export PATH="$PATH:$(go env GOPATH)/bin"

Step 2: Initialize the Go module

bash
cd go/

# WHY this module path?  It matches the go_package option in service.proto.
# The generated code will import common.pb.go using this module path.
go mod init github.com/example/grpc-templates

# Add gRPC dependencies
go get google.golang.org/grpc
go get google.golang.org/protobuf
go get google.golang.org/grpc/health
go get google.golang.org/grpc/health/grpc_health_v1

cd ..

Step 3: Create the output directory

bash
mkdir -p go/gen/catalogpb

Step 4: Generate the code

Run from the project root:

bash
protoc \
    -I. \
    --go_out=go/gen/catalogpb \
    --go_opt=paths=source_relative \
    --go-grpc_out=go/gen/catalogpb \
    --go-grpc_opt=paths=source_relative \
    proto/common.proto \
    proto/service.proto

Flags explained:

FlagPurpose
--go_outOutput directory for message types (*.pb.go)
--go_opt=paths=source_relativeUse the proto file's relative path for the output file location (instead of the go_package option path)
--go-grpc_outOutput directory for service stubs (*_grpc.pb.go)
--go-grpc_opt=paths=source_relativeSame path strategy for gRPC files

Step 5: Verify

bash
cd go/ && go build ./... && echo "Go codegen OK" && cd ..

Common Go Issues

ErrorCauseFix
protoc-gen-go: program not foundPlugin not in PATHexport PATH="$PATH:$(go env GOPATH)/bin"
--go_out: module is provided but package is notgo_package option mismatchEnsure option go_package in .proto matches your module path
undefined: CatalogServiceServerGenerated code not importedImport gen/catalogpb in your server.go

Node.js Code Generation

Node.js has two approaches: static (pre-generate JS files) and dynamic (load .proto files at runtime). The provided server.js and client.js use the dynamic approach, which is simpler and doesn't require a codegen step.

No code generation needed. The proto files are loaded at runtime:

javascript
const protoLoader = require('@grpc/proto-loader');
const grpc = require('@grpc/grpc-js');

const packageDef = protoLoader.loadSync('proto/service.proto', {
    keepCase: true,           // Don't convert field names to camelCase
    longs: String,            // Represent int64 as strings (JS numbers lose precision at 2^53)
    enums: String,            // Represent enums as their string names
    defaults: true,           // Include default values for unset fields
    oneofs: true,             // Support oneof fields
    includeDirs: ['.'],       // Proto import search path
});

const proto = grpc.loadPackageDefinition(packageDef);
const catalog = proto.acme.catalog.v1;

Pros: No build step, proto changes take effect immediately.

Cons: Slightly slower startup (parsing protos at runtime), no TypeScript types.

#### Step 1: Install the Node.js gRPC tools

bash
cd node/
npm install @grpc/grpc-js @grpc/proto-loader
npm install --save-dev grpc-tools grpc_tools_node_protoc_ts
cd ..

#### Step 2: Create the output directory

bash
mkdir -p node/gen

#### Step 3: Generate the code

bash
# Generate JavaScript stubs
npx grpc_tools_node_protoc \
    --js_out=import_style=commonjs,binary:node/gen \
    --grpc_out=grpc_js:node/gen \
    -I. \
    proto/common.proto \
    proto/service.proto

For TypeScript definitions (optional but recommended):

bash
# Generate TypeScript type definitions
npx grpc_tools_node_protoc \
    --ts_out=grpc_js:node/gen \
    -I. \
    proto/common.proto \
    proto/service.proto

#### Step 4: Verify

bash
node -e "const pb = require('./node/gen/proto/service_pb'); console.log('Node codegen OK')"

Dynamic vs Static: Decision Guide

FactorDynamicStatic
Setup complexityNoneRequires codegen step
Startup timeSlower (parses protos)Faster (pre-compiled)
TypeScript supportNoYes (with --ts_out)
Proto change workflowJust restartRegenerate, then restart
CI/CD complexitySimplerNeed codegen in pipeline
Recommended forDev, small servicesProduction, large teams

Makefile for All Languages

Here is a Makefile that generates code for all three languages:

makefile
PROTO_DIR := proto
PROTO_FILES := $(wildcard $(PROTO_DIR)/*.proto)

.PHONY: all python go node clean

all: python go node

python:
	@mkdir -p python/gen
	python -m grpc_tools.protoc \
		-I. \
		--python_out=python/gen \
		--grpc_python_out=python/gen \
		$(PROTO_FILES)
	@touch python/gen/__init__.py
	@echo "Python stubs generated in python/gen/"

go:
	@mkdir -p go/gen/catalogpb
	protoc \
		-I. \
		--go_out=go/gen/catalogpb \
		--go_opt=paths=source_relative \
		--go-grpc_out=go/gen/catalogpb \
		--go-grpc_opt=paths=source_relative \
		$(PROTO_FILES)
	@echo "Go stubs generated in go/gen/catalogpb/"

node:
	@mkdir -p node/gen
	npx grpc_tools_node_protoc \
		--js_out=import_style=commonjs,binary:node/gen \
		--grpc_out=grpc_js:node/gen \
		-I. \
		$(PROTO_FILES)
	@echo "Node stubs generated in node/gen/"

clean:
	rm -rf python/gen go/gen node/gen
	@echo "Generated code cleaned"

Usage:

bash
make all       # Generate for all languages
make python    # Generate Python only
make clean     # Remove all generated code

Buf: A Modern Alternative to protoc

Buf is a modern replacement for protoc that handles dependency management, linting, breaking change detection, and code generation with a single tool.

Install Buf

bash
# macOS
brew install bufbuild/buf/buf

# Linux
curl -sSL "https://github.com/bufbuild/buf/releases/latest/download/buf-Linux-x86_64" -o /usr/local/bin/buf
chmod +x /usr/local/bin/buf

Configure Buf

Create buf.yaml in the project root:

yaml
version: v2
modules:
  - path: proto
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE

Create buf.gen.yaml:

yaml
version: v2
plugins:
  # Python
  - remote: buf.build/protocolbuffers/python
    out: python/gen
  - remote: buf.build/grpc/python
    out: python/gen

  # Go
  - remote: buf.build/protocolbuffers/go
    out: go/gen/catalogpb
    opt: paths=source_relative
  - remote: buf.build/grpc/go
    out: go/gen/catalogpb
    opt: paths=source_relative

  # Node.js (use local plugin — Buf doesn't have an official remote plugin)
  - local: grpc_tools_node_protoc
    out: node/gen
    opt: grpc_js

Generate with Buf

bash
# Lint your protos first (catches common mistakes)
buf lint

# Check for breaking changes against the previous version
buf breaking --against '.git#branch=main'

# Generate code for all languages
buf generate

Why Buf over protoc?

  • Dependency management: Automatically downloads google/protobuf/timestamp.proto etc.
  • Linting: Catches naming convention violations, missing comments, etc.
  • Breaking change detection: Prevents accidentally breaking API contracts.
  • Reproducible builds: buf.lock pins exact dependency versions.

Troubleshooting

"Import not found" Errors

proto/service.proto: Import "proto/common.proto" was not found.

Cause: The -I (include path) flag doesn't cover the directory containing the imported proto.

Fix: Run protoc from the project root with -I. (current directory as include root).

Generated Code Has Wrong Import Paths

Python: The generated service_pb2.py imports common_pb2 using the proto's package path. If your sys.path doesn't include the gen directory, imports fail.

Fix: Add sys.path.insert(0, "gen/") in your server/client code.

Go: The generated code uses the go_package option to determine import paths. If your go.mod module path doesn't match, the build fails.

Fix: Ensure option go_package in the proto matches your go.mod module path + output directory.

"File already exists" When Regenerating

Some tools don't overwrite existing files. Always clean before regenerating:

bash
make clean && make all

Well-Known Types (Timestamp, FieldMask, etc.)

The protos import google/protobuf/timestamp.proto and google/protobuf/field_mask.proto. These are "well-known types" included with protoc.

If protoc can't find them:

bash
# Find where protoc's includes are installed
protoc --version  # Note the version
ls /usr/include/google/protobuf/  # Linux
ls /usr/local/include/google/protobuf/  # macOS (Homebrew)

# Pass the include path explicitly if needed
protoc -I. -I/usr/local/include ...

Part of API Developer Pro. Support: support@datanest.dev

Chapter 2

gRPC Load Balancing — Strategies and Configuration

gRPC uses HTTP/2, which multiplexes many requests over a single long-lived TCP connection. This breaks traditional L4 (TCP) load balancers that distribute connections, not requests. This guide explains why, and what to do about it.


Why gRPC Breaks L4 Load Balancers

Traditional load balancers (AWS NLB, HAProxy in TCP mode) distribute connections:

Client ──[1 TCP connection]──► Load Balancer ──[1 TCP connection]──► Server A
                                                                     Server B (idle)
                                                                     Server C (idle)

With HTTP/1.1 this works because clients open many connections. But gRPC uses HTTP/2, which multiplexes all requests over a single connection. Once the LB routes that connection to Server A, all requests go to Server A forever. Servers B and C sit idle.

The fix: Use an L7 (application-layer) load balancer that understands HTTP/2 and distributes individual requests across backends.


Load Balancing Strategies

Strategy Comparison

StrategyWhereProsCons
Proxy (L7 LB)Separate processLanguage-agnostic, centralized configExtra hop, single point of failure
Client-sideIn the clientNo extra hop, lowest latencyRequires service discovery
LookasideExternal serviceBest of both worldsComplex to operate
Service meshSidecar proxyZero code changesResource overhead per pod

1. Proxy-Based (Envoy, Nginx, Traefik)

An L7 proxy sits between clients and servers. It terminates the HTTP/2 connection from the client and opens separate connections to each backend.

Client ──[HTTP/2]──► Envoy ──[HTTP/2]──► Server A
                           ──[HTTP/2]──► Server B
                           ──[HTTP/2]──► Server C

When to use: Most production deployments. It's the simplest approach that works correctly.

See configs/envoy-load-balancer.yaml for a ready-to-use Envoy configuration.

#### Envoy Key Settings for gRPC

yaml
# These are the critical settings. See the full config in configs/.
clusters:
  - name: grpc_backend
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN  # or LEAST_REQUEST for uneven workloads
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        # WHY explicit_http_config instead of auto_config?
        # auto_config uses ALPN negotiation, which adds latency. Since we KNOW
        # the backends speak HTTP/2 (they're gRPC servers), we skip negotiation.
        explicit_http_config:
          http2_protocol_options:
            max_concurrent_streams: 100
    health_checks:
      - timeout: 2s
        interval: 10s
        unhealthy_threshold: 3
        healthy_threshold: 2
        grpc_health_check: {}  # Uses the standard gRPC health protocol

#### Load Balancing Policies

PolicyHow It WorksBest For
ROUND_ROBINEach request goes to the next backend in rotationUniform request cost
LEAST_REQUESTRoutes to the backend with fewest active requestsVariable request cost
RING_HASHConsistent hashing by a key (e.g., user ID)Session affinity / caching
RANDOMRandom backend selectionSimple, surprisingly effective
MAGLEVGoogle's consistent hashing algorithmHigh-performance ring hash

Recommendation: Start with ROUND_ROBIN. Switch to LEAST_REQUEST if you observe uneven load due to variable RPC durations (e.g., some queries are fast, others are slow).

2. Client-Side Load Balancing

The gRPC client itself distributes requests across multiple backends. This eliminates the proxy hop but requires the client to know about all backends.

#### Python Client-Side LB

python
import grpc

# DNS-based: gRPC resolves the DNS name and round-robins across A records.
# WHY dns:///? The triple-slash tells gRPC to use its built-in DNS resolver
# instead of the OS resolver. This enables watching for DNS changes.
channel = grpc.insecure_channel(
    "dns:///catalog.example.com:50051",
    options=[
        # Pick the load balancing policy.
        ("grpc.lb_policy_name", "round_robin"),
        # How often to re-resolve DNS (milliseconds).
        ("grpc.dns_min_time_between_resolutions_ms", 10000),
    ],
)

#### Go Client-Side LB

go
import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/balancer/roundrobin"
)

conn, err := grpc.Dial(
    "dns:///catalog.example.com:50051",
    grpc.WithDefaultServiceConfig(
        // The service config is JSON. This sets round_robin as the LB policy.
        `{"loadBalancingConfig": [{"round_robin":{}}]}`,
    ),
    grpc.WithTransportCredentials(insecure.NewCredentials()),
)

#### Node.js Client-Side LB

javascript
const grpc = require('@grpc/grpc-js');

// Node.js gRPC supports round_robin via channel options.
const client = new CatalogServiceClient(
    'dns:///catalog.example.com:50051',
    grpc.credentials.createInsecure(),
    {
        'grpc.lb_policy_name': 'round_robin',
        'grpc.service_config': JSON.stringify({
            loadBalancingConfig: [{ round_robin: {} }],
        }),
    }
);

#### Service Discovery Options

Client-side LB needs to know the backend addresses. Options:

Discovery MethodHow It WorksExample
DNS A recordsMultiple A records for one hostnamecatalog.example.com → 10.0.0.1, 10.0.0.2, 10.0.0.3
DNS SRV recordsService records with port + priority_grpc._tcp.catalog.example.com
Kubernetes headless serviceDNS returns pod IPs directlycatalog.default.svc.cluster.local
Custom resolverRegister a gRPC name resolver pluginConsul, etcd, ZooKeeper

Kubernetes tip: Use a headless Service (clusterIP: None) so DNS returns individual pod IPs instead of the single ClusterIP. This lets client-side LB work correctly.

yaml
apiVersion: v1
kind: Service
metadata:
  name: catalog-grpc
spec:
  clusterIP: None  # Headless — DNS returns pod IPs
  selector:
    app: catalog
  ports:
    - port: 50051
      targetPort: 50051

3. Service Mesh (Istio, Linkerd)

A service mesh injects a sidecar proxy (Envoy for Istio, Linkerd-proxy for Linkerd) alongside every pod. The sidecar handles L7 load balancing transparently — your application code doesn't change.

Pod A                          Pod B                          Pod C
┌─────────────────┐           ┌─────────────────┐           ┌─────────────────┐
│ App  →  Sidecar ├───────────┤ Sidecar  →  App │           │ Sidecar  →  App │
└─────────────────┘           └─────────────────┘           └─────────────────┘

When to use: When you have many services (>10) and want consistent load balancing, mTLS, and observability without changing application code.

Trade-off: Each sidecar consumes ~50MB RAM and adds ~1ms latency per hop. For a 3-service architecture, it's overkill. For 50+ services, it pays for itself.


Health Checking

gRPC has a standard health checking protocol (grpc.health.v1.Health). Load balancers and orchestrators use it to route traffic only to healthy backends.

See configs/grpc-health-check.yaml for Kubernetes, Docker Compose, and AWS configurations.

Implementing the Health Service

The health service is separate from your business logic. It reports the status of your service (and optionally individual sub-services).

python
# Python — using grpc_health
from grpc_health.v1 import health_pb2, health_pb2_grpc, health

health_servicer = health.HealthServicer()
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)

# Set status for the overall service
health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)

# Set status for a specific service (optional)
health_servicer.set(
    "acme.catalog.v1.CatalogService",
    health_pb2.HealthCheckResponse.SERVING,
)
go
// Go — using grpc/health
import (
    "google.golang.org/grpc/health"
    healthpb "google.golang.org/grpc/health/grpc_health_v1"
)

healthServer := health.NewServer()
healthpb.RegisterHealthServer(grpcServer, healthServer)

// SERVING = ready to accept requests
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
healthServer.SetServingStatus(
    "acme.catalog.v1.CatalogService",
    healthpb.HealthCheckResponse_SERVING,
)

Health Status Values

StatusMeaningLB Action
SERVINGReady to accept requestsRoute traffic
NOT_SERVINGAlive but not ready (e.g., warming up)Stop routing
SERVICE_UNKNOWNHealth service doesn't know about this serviceTreat as unhealthy
(connection refused)Process is downRemove from pool

Graceful Shutdown Sequence

When shutting down a gRPC server, the order matters:

1. Set health status to NOT_SERVING
   └─ LB stops sending new requests (after next health check)

2. Wait for drain period (e.g., 15 seconds)
   └─ In-flight requests complete

3. Stop accepting new connections
   └─ server.stop(grace_period)

4. Wait for remaining RPCs to finish (or hit grace period)
   └─ Forcefully terminate if grace period expires

5. Exit
python
# Python graceful shutdown
import signal

def shutdown(signum, frame):
    # Step 1: Mark unhealthy so LB stops sending traffic
    health_servicer.set("", health_pb2.HealthCheckResponse.NOT_SERVING)

    # Step 2: Grace period for in-flight RPCs
    # server.stop() returns an Event that fires when all RPCs complete
    # or the grace period expires — whichever comes first
    stopped = server.stop(grace=15)
    stopped.wait()

signal.signal(signal.SIGTERM, shutdown)
go
// Go graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit

// Step 1: Mark unhealthy
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)

// Step 2: GracefulStop waits for active RPCs to complete.
// Unlike Stop(), it doesn't forcefully kill connections.
grpcServer.GracefulStop()

Connection Management

Keep-Alive Settings

gRPC connections are long-lived. Without keep-alive, firewalls and NAT gateways silently drop idle connections (typically after 5-15 minutes).

python
# Python — keep-alive on server
server = grpc.server(
    futures.ThreadPoolExecutor(max_workers=10),
    options=[
        # Send pings every 60 seconds if no data is being sent.
        ("grpc.keepalive_time_ms", 60000),
        # Wait 20 seconds for a ping response before considering dead.
        ("grpc.keepalive_timeout_ms", 20000),
        # Allow pings even when there are no active RPCs.
        # WHY: Without this, idle connections are silently dropped by
        # firewalls, and the next RPC fails with a confusing error.
        ("grpc.keepalive_permit_without_calls", 1),
        # Allow clients to send pings every 30 seconds.
        # (Server enforces a minimum interval to prevent ping floods.)
        ("grpc.http2.min_ping_interval_without_data_ms", 30000),
    ],
)

Max Concurrent Streams

HTTP/2 allows limiting how many concurrent RPCs share a single connection. When the limit is reached, the client opens a new connection (enabling better load distribution).

python
# Limit to 100 concurrent streams per connection.
# WHY: With unlimited streams, one connection serves all RPCs,
# defeating client-side load balancing. With 100, the client
# opens new connections as load increases, and those connections
# are distributed across backends by the LB policy.
("grpc.max_concurrent_streams", 100)

Choosing the Right Strategy

Are you on Kubernetes with >10 services?
  └─ YES → Service mesh (Istio/Linkerd)

Are you on Kubernetes with 2-10 services?
  └─ YES → Do you already have an ingress controller?
              ├─ YES → Envoy/Nginx L7 proxy
              └─ NO  → Client-side with headless Services

Are you running on bare metal / VMs?
  └─ Use Envoy as a standalone L7 proxy

Is this a single client → single server?
  └─ You don't need load balancing yet. Add it later.

Quick Reference

ScenarioRecommended StrategyConfig Provided
Kubernetes, few servicesClient-side with headless ServiceGo/Python examples above
Kubernetes, many servicesService mesh (Istio)N/A (Istio handles it)
Any platform, need controlEnvoy L7 proxyconfigs/envoy-load-balancer.yaml
Development / testingNo LB neededDirect connection

Part of API Developer Pro. Support: support@datanest.dev

Chapter 3
🔒 Available in full product

gRPC Streaming Patterns — A Practical Guide

You’ve reached the end of the free preview

Get the full gRPC Service Templates and unlock everything.

All Chapters

Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.

Full Tool Suite

Access all interactive tools with complete data, all workload profiles, and the full scenario library.

Source Files

Downloadable source code, configuration files, and working examples from every chapter.

Lifetime Updates

Free updates for life. Every new chapter, tool, and improvement included.

Buy Now — $39 →
📦 Free sample included — download another copy for the full product.
gRPC Service Templates v1.0.0 — Free Preview