gRPC Service Design: Patterns, Streaming, and Best Practices

Type: Software Reference Confidence: 0.90 Sources: 8 Verified: 2026-02-24 Freshness: 2026-02-24

TL;DR

Constraints

Quick Reference

RPC TypeProto SyntaxUse CaseComplexityError HandlingFlow Control
Unaryrpc Get(Req) returns (Resp)CRUD, lookups, commandsLowSingle status code + detailsN/A
Server streamingrpc List(Req) returns (stream Resp)Feed updates, large result sets, log tailingMediumStatus on stream close; per-message errors via oneofBackpressure built-in
Client streamingrpc Upload(stream Req) returns (Resp)File upload, batch ingest, telemetryMediumServer responds after all messages; early cancel possibleClient controls send rate
Bidirectional streamingrpc Chat(stream Req) returns (stream Resp)Real-time chat, multiplayer sync, live dashboardsHighBoth sides can error independently; need app-level acksIndependent read/write streams

gRPC Status Codes Quick Lookup

CodeConstantWhen to Use
0OKSuccess
3INVALID_ARGUMENTClient sent malformed request
5NOT_FOUNDRequested resource does not exist
7PERMISSION_DENIEDCaller lacks permission (authenticated but unauthorized)
8RESOURCE_EXHAUSTEDRate limit hit or quota exceeded
13INTERNALUnexpected server error (do not expose details to client)
14UNAVAILABLETransient failure — client should retry with backoff
16UNAUTHENTICATEDMissing or invalid auth credentials

Decision Tree

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

Step-by-Step Guide

1. Define your proto service contract

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

2. Generate language-specific stubs

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

3. Implement server with interceptors

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

4. Implement proper error handling

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

5. Implement client with deadlines and retries

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

Code Examples

Go: Server Streaming

// 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()
        }
    }
}

Python: Unary Client with Error Handling

# 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

Node.js: Bidirectional Streaming

// 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" });

Anti-Patterns

Wrong: REST-style resource URLs in gRPC methods

// BAD -- gRPC methods are not HTTP endpoints
service OrderService {
  rpc GetOrdersSlashOrderIdSlashItems(GetRequest) returns (Response);
  rpc PostOrders(CreateRequest) returns (Response);
}

Correct: Use clear VerbNoun method names

// GOOD -- follow Google AIP naming: VerbNoun
service OrderService {
  rpc GetOrder(GetOrderRequest) returns (Order);
  rpc ListOrders(ListOrdersRequest) returns (ListOrdersResponse);
  rpc CreateOrder(CreateOrderRequest) returns (Order);
}

Wrong: Reusing deleted field numbers

// 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!
}

Correct: Reserve deleted field numbers

// GOOD -- reserved prevents accidental reuse
message Order {
  string id = 1;
  string name = 2;
  reserved 3;
  reserved "old_field";
  int64 total = 4;
}

Wrong: No deadline on RPC calls

// BAD -- no deadline, call can hang forever
resp, err := client.GetOrder(context.Background(), req)

Correct: Always set a deadline

// GOOD -- deadline ensures call fails fast
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetOrder(ctx, req)

Wrong: Generic Request/Response for all RPCs

// 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);
}

Correct: Dedicated request/response per RPC

// GOOD -- each RPC gets its own messages
service MyService {
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

Common Pitfalls

Diagnostic Commands

# 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 History & Compatibility

VersionStatusBreaking ChangesMigration Notes
gRPC 1.60+ (2024)CurrentStrict keepalive enforcementAdd KeepaliveEnforcementPolicy to server options
protoc 3.21+ / protobuf-go v2CurrentGo module path changedMigrate from github.com/golang/protobuf
protoc 3.15+ (2021)Stableoptional keyword reintroduced in proto3Can use optional for explicit presence tracking
gRPC-Go 1.57+ (2023)Currentgrpc.Dial deprecatedReplace with grpc.NewClient
gRPC-Web 1.5+CurrentNoneRequired for browser clients

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Internal microservice communicationPublic APIs consumed by browsersREST + OpenAPI
Strict contract enforcement across teamsRapid prototyping with frequently changing schemasREST or GraphQL
Low-latency, high-throughput RPCSimple CRUD with minimal client diversityREST
Real-time bidirectional data (chat, sync)Occasional webhook-style callbacksWebhooks or SSE
Polyglot services needing shared contractTeam unfamiliar with protobuf toolingREST + JSON Schema
Streaming large datasets (logs, events)Small payloads with aggressive HTTP caching needsREST with Cache-Control
Mobile-to-backend with bandwidth constraintsIoT devices without HTTP/2MQTT or CoAP

Important Caveats

Related Units