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.
protoc)The protoc compiler parses .proto files and generates language-specific code. Each language also needs a gRPC plugin.
| OS | Install Command |
|---|---|
| macOS (Homebrew) | brew install protobuf |
| Ubuntu / Debian | sudo apt install -y protobuf-compiler |
| Fedora / RHEL | sudo dnf install -y protobuf-compiler |
| Windows (Chocolatey) | choco install protoc |
| From source | github.com/protocolbuffers/protobuf/releases |
Verify the installation:
protoc --version
# libprotoc 25.x or laterAfter 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)
pip install grpcio grpcio-tools grpcio-health-checkinggrpcio-tools includes both the protoc compiler and the Python gRPC plugin, so you don't need a separate protoc install for Python.
mkdir -p python/genRun from the project root directory:
# 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.protoThe generated service_pb2_grpc.py will contain:
import proto.common_pb2 as common__pb2This 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:
touch python/gen/__init__.pyThen in server.py and client.py, add the gen directory to the path:
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "gen"))python -c "from python.gen import service_pb2, service_pb2_grpc; print('Python codegen OK')"# 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@latestMake sure $GOPATH/bin (or $HOME/go/bin) is in your $PATH:
export PATH="$PATH:$(go env GOPATH)/bin"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 ..mkdir -p go/gen/catalogpbRun from the project root:
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.protoFlags explained:
| Flag | Purpose |
|---|---|
--go_out | Output directory for message types (*.pb.go) |
--go_opt=paths=source_relative | Use the proto file's relative path for the output file location (instead of the go_package option path) |
--go-grpc_out | Output directory for service stubs (*_grpc.pb.go) |
--go-grpc_opt=paths=source_relative | Same path strategy for gRPC files |
cd go/ && go build ./... && echo "Go codegen OK" && cd ..| Error | Cause | Fix |
|---|---|---|
protoc-gen-go: program not found | Plugin not in PATH | export PATH="$PATH:$(go env GOPATH)/bin" |
--go_out: module is provided but package is not | go_package option mismatch | Ensure option go_package in .proto matches your module path |
undefined: CatalogServiceServer | Generated code not imported | Import gen/catalogpb in your server.go |
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:
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
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
mkdir -p node/gen#### Step 3: Generate the code
# 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.protoFor TypeScript definitions (optional but recommended):
# 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
node -e "const pb = require('./node/gen/proto/service_pb'); console.log('Node codegen OK')"| Factor | Dynamic | Static |
|---|---|---|
| Setup complexity | None | Requires codegen step |
| Startup time | Slower (parses protos) | Faster (pre-compiled) |
| TypeScript support | No | Yes (with --ts_out) |
| Proto change workflow | Just restart | Regenerate, then restart |
| CI/CD complexity | Simpler | Need codegen in pipeline |
| Recommended for | Dev, small services | Production, large teams |
Here is a Makefile that generates code for all three languages:
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:
make all # Generate for all languages
make python # Generate Python only
make clean # Remove all generated codeBuf is a modern replacement for protoc that handles dependency management, linting, breaking change detection, and code generation with a single tool.
# 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/bufCreate buf.yaml in the project root:
version: v2
modules:
- path: proto
lint:
use:
- DEFAULT
breaking:
use:
- FILECreate buf.gen.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# 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 generateWhy Buf over protoc?
google/protobuf/timestamp.proto etc.buf.lock pins exact dependency versions.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).
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.
Some tools don't overwrite existing files. Always clean before regenerating:
make clean && make allThe 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:
# 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
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.
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.
| Strategy | Where | Pros | Cons |
|---|---|---|---|
| Proxy (L7 LB) | Separate process | Language-agnostic, centralized config | Extra hop, single point of failure |
| Client-side | In the client | No extra hop, lowest latency | Requires service discovery |
| Lookaside | External service | Best of both worlds | Complex to operate |
| Service mesh | Sidecar proxy | Zero code changes | Resource overhead per pod |
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
# 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
| Policy | How It Works | Best For |
|---|---|---|
ROUND_ROBIN | Each request goes to the next backend in rotation | Uniform request cost |
LEAST_REQUEST | Routes to the backend with fewest active requests | Variable request cost |
RING_HASH | Consistent hashing by a key (e.g., user ID) | Session affinity / caching |
RANDOM | Random backend selection | Simple, surprisingly effective |
MAGLEV | Google's consistent hashing algorithm | High-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).
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
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
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
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 Method | How It Works | Example |
|---|---|---|
| DNS A records | Multiple A records for one hostname | catalog.example.com → 10.0.0.1, 10.0.0.2, 10.0.0.3 |
| DNS SRV records | Service records with port + priority | _grpc._tcp.catalog.example.com |
| Kubernetes headless service | DNS returns pod IPs directly | catalog.default.svc.cluster.local |
| Custom resolver | Register a gRPC name resolver plugin | Consul, 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.
apiVersion: v1
kind: Service
metadata:
name: catalog-grpc
spec:
clusterIP: None # Headless — DNS returns pod IPs
selector:
app: catalog
ports:
- port: 50051
targetPort: 50051A 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.
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.
The health service is separate from your business logic. It reports the status of your service (and optionally individual sub-services).
# 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 — 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,
)| Status | Meaning | LB Action |
|---|---|---|
SERVING | Ready to accept requests | Route traffic |
NOT_SERVING | Alive but not ready (e.g., warming up) | Stop routing |
SERVICE_UNKNOWN | Health service doesn't know about this service | Treat as unhealthy |
(connection refused) | Process is down | Remove from pool |
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 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 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()gRPC connections are long-lived. Without keep-alive, firewalls and NAT gateways silently drop idle connections (typically after 5-15 minutes).
# 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),
],
)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).
# 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)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.
| Scenario | Recommended Strategy | Config Provided |
|---|---|---|
| Kubernetes, few services | Client-side with headless Service | Go/Python examples above |
| Kubernetes, many services | Service mesh (Istio) | N/A (Istio handles it) |
| Any platform, need control | Envoy L7 proxy | configs/envoy-load-balancer.yaml |
| Development / testing | No LB needed | Direct connection |
Part of API Developer Pro. Support: support@datanest.dev
Get the full gRPC Service Templates and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.