protoc --go_out=. --go-grpc_out=. service.proto (or language-specific plugin)reserved to retire fields.reserved to prevent accidental reuse[deprecated=true] and reserve the field number| RPC Type | Proto Syntax | Use Case | Complexity | Error Handling | Flow Control |
|---|---|---|---|---|---|
| Unary | rpc Get(Req) returns (Resp) | CRUD, lookups, commands | Low | Single status code + details | N/A |
| Server streaming | rpc List(Req) returns (stream Resp) | Feed updates, large result sets, log tailing | Medium | Status on stream close; per-message errors via oneof | Backpressure built-in |
| Client streaming | rpc Upload(stream Req) returns (Resp) | File upload, batch ingest, telemetry | Medium | Server responds after all messages; early cancel possible | Client controls send rate |
| Bidirectional streaming | rpc Chat(stream Req) returns (stream Resp) | Real-time chat, multiplayer sync, live dashboards | High | Both sides can error independently; need app-level acks | Independent read/write streams |
| Code | Constant | When to Use |
|---|---|---|
| 0 | OK | Success |
| 3 | INVALID_ARGUMENT | Client sent malformed request |
| 5 | NOT_FOUND | Requested resource does not exist |
| 7 | PERMISSION_DENIED | Caller lacks permission (authenticated but unauthorized) |
| 8 | RESOURCE_EXHAUSTED | Rate limit hit or quota exceeded |
| 13 | INTERNAL | Unexpected server error (do not expose details to client) |
| 14 | UNAVAILABLE | Transient failure — client should retry with backoff |
| 16 | UNAUTHENTICATED | Missing or invalid auth credentials |
START: What kind of API communication do you need?
+-- Browser clients calling your API directly?
| +-- YES -> Use REST or GraphQL; gRPC requires gRPC-Web proxy
| +-- NO
+-- Internal microservice-to-microservice communication?
| +-- YES -> gRPC is ideal. Continue to streaming type selection.
| +-- NO
+-- Polyglot environment needing strict contracts?
| +-- YES -> gRPC with shared proto repository
| +-- NO -> Consider REST with OpenAPI for simpler tooling
STREAMING TYPE SELECTION:
+-- Single request, single response (CRUD/query)?
| +-- YES -> Unary RPC
| +-- NO
+-- Server pushes multiple results for one request?
| +-- YES -> Server streaming (search results, event feed)
| +-- NO
+-- Client sends many items, server responds once?
| +-- YES -> Client streaming (batch upload, telemetry)
| +-- NO
+-- Both sides send/receive independently?
+-- YES -> Bidirectional streaming (chat, live sync)
LOAD BALANCING:
+-- Using a service mesh (Istio/Linkerd)?
| +-- YES -> Mesh handles L7 gRPC load balancing automatically
| +-- NO
+-- Have a proxy (Envoy/nginx)?
| +-- YES -> Configure proxy for HTTP/2 gRPC routing
| +-- NO
+-- DEFAULT -> Use client-side round-robin via gRPC name resolver
Start with the .proto file. This is the single source of truth for your API. Follow Google's AIP naming conventions: services are PascalCase, methods are VerbNoun, messages use PascalCase with snake_case fields. [src2]
syntax = "proto3";
package myapp.orders.v1;
option go_package = "github.com/myorg/myapp/gen/orders/v1";
import "google/protobuf/timestamp.proto";
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (Order);
rpc GetOrder(GetOrderRequest) returns (Order);
rpc WatchOrders(WatchOrdersRequest) returns (stream OrderEvent);
}
message Order {
string id = 1;
string customer_id = 2;
repeated OrderItem items = 3;
OrderStatus status = 4;
google.protobuf.Timestamp created_at = 5;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
}
Verify: buf lint proto/ -> no warnings
Use protoc with language-specific plugins, or use Buf for a modern workflow. [src1]
# Go
protoc --go_out=. --go-grpc_out=. \
--go_opt=paths=source_relative \
--go-grpc_opt=paths=source_relative \
proto/orders/v1/service.proto
# Python
python -m grpc_tools.protoc -I proto \
--python_out=gen --grpc_python_out=gen \
proto/orders/v1/service.proto
# Node.js
grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:gen \
--grpc_out=grpc_js:gen \
proto/orders/v1/service.proto
Verify: Generated files appear in output directory with correct package names
Add interceptors for cross-cutting concerns. Order matters: recovery outermost so panics don't crash the process. [src4] [src7]
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
recoveryInterceptor, // Catch panics first
metricsInterceptor, // Track all requests
loggingInterceptor, // Log all requests
authInterceptor, // Validate tokens
),
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionIdle: 5 * time.Minute,
Time: 2 * time.Hour,
Timeout: 20 * time.Second,
}),
)
reflection.Register(server) // Enable grpcurl debugging
Verify: grpcurl -plaintext localhost:50051 list -> shows registered services
Return rich error details using gRPC status codes and the errdetails package. Never expose internal errors to clients. [src8]
st := status.New(codes.InvalidArgument, "order ID is required")
st, _ = st.WithDetails(&errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{Field: "id", Description: "must be non-empty"},
},
})
return nil, st.Err()
Verify: grpcurl -d '{"id":""}' localhost:50051 myapp.orders.v1.OrderService/GetOrder -> returns INVALID_ARGUMENT with field violation details
Every RPC call must have a deadline. Configure retry policies declaratively via service config. [src3]
// Always set a deadline on every RPC call
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetOrder(ctx, &GetOrderRequest{Id: orderID})
Verify: Call with an unreachable server and confirm retry + timeout behavior
// Input: WatchOrdersRequest with customer_id
// Output: Stream of OrderEvent messages until client cancels
func (s *orderServer) WatchOrders(
req *WatchOrdersRequest,
stream OrderService_WatchOrdersServer,
) error {
ch := s.eventBus.Subscribe(req.CustomerId)
defer s.eventBus.Unsubscribe(ch)
for {
select {
case event := <-ch:
if err := stream.Send(event); err != nil {
return status.Errorf(codes.Internal, "send failed: %v", err)
}
case <-stream.Context().Done():
return status.FromContextError(stream.Context().Err()).Err()
}
}
}
# Input: order_id string
# Output: Order object or raises appropriate exception
# Requires: grpcio>=1.68.0, grpcio-tools>=1.68.0
import grpc
from orders.v1 import service_pb2, service_pb2_grpc
def get_order(order_id: str) -> service_pb2.Order:
channel = grpc.insecure_channel("localhost:50051")
stub = service_pb2_grpc.OrderServiceStub(channel)
try:
return stub.GetOrder(
service_pb2.GetOrderRequest(id=order_id),
timeout=5.0,
)
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.NOT_FOUND:
print(f"Order {order_id} not found")
elif e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
print("Request timed out")
raise
// Input: bidirectional stream of ChatMessage
// Output: bidirectional stream of ChatMessage
// Requires: @grpc/grpc-js@^1.12.0, @grpc/proto-loader@^0.7.0
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const packageDef = protoLoader.loadSync("chat.proto");
const proto = grpc.loadPackageDefinition(packageDef);
const client = new proto.ChatService(
"localhost:50051", grpc.credentials.createInsecure()
);
const call = client.chat();
call.on("data", (msg) => console.log(`Received: ${msg.text}`));
call.on("error", (err) => {
if (err.code === grpc.status.UNAVAILABLE)
console.log("Server unavailable, reconnecting...");
});
call.write({ text: "Hello", sender: "user1" });
// BAD -- gRPC methods are not HTTP endpoints
service OrderService {
rpc GetOrdersSlashOrderIdSlashItems(GetRequest) returns (Response);
rpc PostOrders(CreateRequest) returns (Response);
}
// GOOD -- follow Google AIP naming: VerbNoun
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
rpc CreateOrder(CreateOrderRequest) returns (Order);
}
// BAD -- field 3 was deleted, now reused with different type
message Order {
string id = 1;
string name = 2;
// was: string old_field = 3; (deleted)
int64 total = 3; // Reuses field number 3!
}
// GOOD -- reserved prevents accidental reuse
message Order {
string id = 1;
string name = 2;
reserved 3;
reserved "old_field";
int64 total = 4;
}
// BAD -- no deadline, call can hang forever
resp, err := client.GetOrder(context.Background(), req)
// GOOD -- deadline ensures call fails fast
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetOrder(ctx, req)
// BAD -- prevents independent evolution of each RPC
message Request { string type = 1; bytes payload = 2; }
service MyService {
rpc DoSomething(Request) returns (Response);
rpc DoOtherThing(Request) returns (Response);
}
// GOOD -- each RPC gets its own messages
service MyService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
ENUM_NAME_UNSPECIFIED = 0;. [src5]grpcurl cannot introspect services. Fix: add reflection.Register(server) in Go or add_reflection in Python. [src1]Send() return values and respect context.Done(). [src3]int64 cents or google.type.Money. [src5]status.Error(codes.Internal, "internal error"). [src8]package myapp.orders.v1;. [src2]# List all services on a running gRPC server
grpcurl -plaintext localhost:50051 list
# Describe a specific service and its methods
grpcurl -plaintext localhost:50051 describe myapp.orders.v1.OrderService
# Call a unary RPC with JSON payload
grpcurl -plaintext -d '{"id": "order-123"}' \
localhost:50051 myapp.orders.v1.OrderService/GetOrder
# Health check
grpc_health_probe -addr=localhost:50051
# Lint proto files with Buf
buf lint proto/
# Detect breaking changes against main branch
buf breaking proto/ --against .git#branch=main
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| gRPC 1.60+ (2024) | Current | Strict keepalive enforcement | Add KeepaliveEnforcementPolicy to server options |
| protoc 3.21+ / protobuf-go v2 | Current | Go module path changed | Migrate from github.com/golang/protobuf |
| protoc 3.15+ (2021) | Stable | optional keyword reintroduced in proto3 | Can use optional for explicit presence tracking |
| gRPC-Go 1.57+ (2023) | Current | grpc.Dial deprecated | Replace with grpc.NewClient |
| gRPC-Web 1.5+ | Current | None | Required for browser clients |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Internal microservice communication | Public APIs consumed by browsers | REST + OpenAPI |
| Strict contract enforcement across teams | Rapid prototyping with frequently changing schemas | REST or GraphQL |
| Low-latency, high-throughput RPC | Simple CRUD with minimal client diversity | REST |
| Real-time bidirectional data (chat, sync) | Occasional webhook-style callbacks | Webhooks or SSE |
| Polyglot services needing shared contract | Team unfamiliar with protobuf tooling | REST + JSON Schema |
| Streaming large datasets (logs, events) | Small payloads with aggressive HTTP caching needs | REST with Cache-Control |
| Mobile-to-backend with bandwidth constraints | IoT devices without HTTP/2 | MQTT or CoAP |
optional keyword (protoc 3.15+) or wrapper types to distinguish "not set" from "set to zero/empty".grpc.health.v1.Health) — implement it for Kubernetes readiness/liveness probes and load balancer health checks.