gRPC is a high-performance RPC framework using Protocol Buffers for serialization and HTTP/2 for transport. It supports unary, server streaming, client streaming, and bidirectional streaming. Define services in .proto files, generate type-safe client/server code, and get built-in features like TLS, deadlines, and interceptors. Use gRPC for internal microservices; use REST or gRPC-Web for browser-facing APIs.
1. What is gRPC?
gRPC (gRPC Remote Procedure Call) is an open-source high-performance RPC framework originally developed by Google. It uses Protocol Buffers (protobuf) as its interface definition language and serialization format, and runs on HTTP/2 for transport. This combination gives gRPC binary efficiency, multiplexed connections, header compression, and native streaming support.
Unlike REST where you design around resources and HTTP verbs, gRPC lets you define services with methods that clients call as if they were local function calls. The protobuf compiler generates strongly typed client and server stubs in over a dozen languages, eliminating manual serialization code.
2. Protocol Buffers Primer
Protocol Buffers (protobuf) is a language-neutral, platform-neutral mechanism for serializing structured data. You define your data shape once in a .proto file, then generate code for any target language.
Basic Message Definition
syntax = "proto3";
package user;
message User {
string id = 1;
string name = 2;
string email = 3;
int32 age = 4;
repeated string roles = 5; // list of strings
Address address = 6; // nested message
}
message Address {
string street = 1;
string city = 2;
string country = 3;
string zip_code = 4;
}Each field has a type, a name, and a unique field number. Field numbers are used in the binary encoding and must not change once your message type is in use. The repeated keyword indicates a list. Proto3 makes all fields optional by default.
Protobuf vs JSON Size Comparison
| Format | Payload Size | Encode Time | Decode Time |
|---|---|---|---|
| JSON | ~250 bytes | ~1.2 ms | ~1.5 ms |
| Protobuf | ~48 bytes | ~0.15 ms | ~0.12 ms |
3. gRPC vs REST: When to Use Which
| Criteria | gRPC | REST |
|---|---|---|
| Serialization | Protobuf (binary) | JSON (text) |
| Transport | HTTP/2 | HTTP/1.1 or HTTP/2 |
| Streaming | Built-in (4 patterns) | SSE or WebSocket |
| Code generation | Native (protoc) | Optional (OpenAPI) |
| Browser support | Via gRPC-Web proxy | Native |
| Best for | Microservices, streaming, polyglot | Public APIs, CRUD, simple integrations |
Rule of thumb: Use gRPC for internal service-to-service communication where performance and type safety matter. Use REST for public APIs consumed by third-party developers or browser clients without a proxy layer.
4. Defining Services with .proto Files
A gRPC service definition declares the RPC methods available to clients. Each method specifies a request and response message type, and optionally uses the stream keyword.
syntax = "proto3";
package order;
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (Order); // unary
rpc GetOrder (GetOrderRequest) returns (Order); // unary
rpc ListOrders (ListOrdersRequest) returns (stream Order); // server stream
rpc UploadLineItems (stream LineItem) returns (UploadSummary); // client stream
rpc OrderChat (stream ChatMessage) returns (stream ChatMessage); // bidi
}
message CreateOrderRequest {
string customer_id = 1;
repeated LineItem items = 2;
}
message LineItem {
string product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
message Order {
string id = 1;
string customer_id = 2;
repeated LineItem items = 3;
double total = 4;
OrderStatus status = 5;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_CONFIRMED = 2;
ORDER_STATUS_SHIPPED = 3;
}Compile the proto file to generate language-specific code:
# Generate Go code
protoc --go_out=. --go-grpc_out=. proto/order.proto
# Generate Node.js/TypeScript code
protoc --ts_out=./src/gen proto/order.proto
# Generate Python code
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. proto/order.proto5. Streaming Patterns Explained
| Pattern | Proto Syntax | Use Cases |
|---|---|---|
| Unary | rpc Get(Req) returns (Resp) | CRUD, auth, lookups |
| Server streaming | rpc List(Req) returns (stream Resp) | Feeds, log tailing, large result sets |
| Client streaming | rpc Upload(stream Req) returns (Resp) | File uploads, batch ingestion, telemetry |
| Bidirectional | rpc Chat(stream Msg) returns (stream Msg) | Chat, collaborative editing, game state |
The stream keyword in the proto definition controls the pattern. Both sides read and write independently in bidirectional mode, and the two streams operate over a single HTTP/2 connection.
6. gRPC in Node.js / TypeScript
The @grpc/grpc-js package is the official pure-JavaScript gRPC client and server. Pair it with @grpc/proto-loader for dynamic loading or ts-proto for generated TypeScript types.
Server Implementation
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
const PROTO_PATH = "./proto/order.proto";
const packageDef = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDef) as any;
function createOrder(
call: grpc.ServerUnaryCall<any, any>,
callback: grpc.sendUnaryData<any>
) {
const request = call.request;
const order = {
id: "ord-" + Date.now(),
customer_id: request.customer_id,
items: request.items,
total: request.items.reduce(
(sum: number, item: any) => sum + item.quantity * item.unit_price, 0
),
status: "ORDER_STATUS_PENDING",
};
callback(null, order);
}
function listOrders(
call: grpc.ServerWritableStream<any, any>
) {
// Server streaming: push orders one by one
const orders = getOrdersFromDB(call.request.customer_id);
for (const order of orders) {
call.write(order);
}
call.end();
}
const server = new grpc.Server();
server.addService(proto.order.OrderService.service, {
createOrder,
listOrders,
});
server.bindAsync(
"0.0.0.0:50051",
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err) throw err;
console.log("gRPC server running on port " + port);
}
);Client Implementation
const client = new proto.order.OrderService(
"localhost:50051",
grpc.credentials.createInsecure()
);
// Unary call
client.createOrder(
{ customer_id: "cust-1", items: [{ product_id: "p-1", quantity: 2, unit_price: 29.99 }] },
(err: any, response: any) => {
if (err) return console.error("Error:", err.message);
console.log("Order created:", response.id);
}
);
// Server streaming
const stream = client.listOrders({ customer_id: "cust-1" });
stream.on("data", (order: any) => console.log("Order:", order.id));
stream.on("end", () => console.log("All orders received"));
stream.on("error", (err: any) => console.error("Error:", err.message));7. gRPC in Go
Go has first-class gRPC support through google.golang.org/grpc. The generated code provides interfaces you implement for the server and ready-to-use clients with full type safety.
Server Implementation
package main
import (
"context"
"fmt"
"log"
"net"
"time"
"google.golang.org/grpc"
pb "myapp/gen/order"
)
type orderServer struct {
pb.UnimplementedOrderServiceServer
}
func (s *orderServer) CreateOrder(
ctx context.Context,
req *pb.CreateOrderRequest,
) (*pb.Order, error) {
order := &pb.Order{
Id: fmt.Sprintf("ord-%d", time.Now().UnixMilli()),
CustomerId: req.CustomerId,
Items: req.Items,
Status: pb.OrderStatus_ORDER_STATUS_PENDING,
}
return order, nil
}
func (s *orderServer) ListOrders(
req *pb.ListOrdersRequest,
stream pb.OrderService_ListOrdersServer,
) error {
orders := getOrdersFromDB(req.CustomerId)
for _, order := range orders {
if err := stream.Send(order); err != nil {
return err
}
}
return nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
srv := grpc.NewServer()
pb.RegisterOrderServiceServer(srv, &orderServer{})
log.Println("gRPC server listening on :50051")
if err := srv.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}Client Implementation
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewOrderServiceClient(conn)
// Unary call with deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
order, err := client.CreateOrder(ctx, &pb.CreateOrderRequest{
CustomerId: "cust-1",
Items: []*pb.LineItem{
{ProductId: "p-1", Quantity: 2, UnitPrice: 29.99},
},
})
fmt.Println("Created order:", order.Id)
// Server streaming — read until EOF
stream, _ := client.ListOrders(ctx, &pb.ListOrdersRequest{CustomerId: "cust-1"})
for {
o, err := stream.Recv()
if err == io.EOF { break }
if err != nil { log.Fatal(err) }
fmt.Println("Order:", o.Id)
}8. gRPC in Python
Python uses grpcio and grpcio-tools packages. The generated code provides typed stubs for both sync and async usage.
Server Implementation
import grpc
from concurrent import futures
import time
import order_pb2
import order_pb2_grpc
class OrderServicer(order_pb2_grpc.OrderServiceServicer):
def CreateOrder(self, request, context):
total = sum(
item.quantity * item.unit_price
for item in request.items
)
return order_pb2.Order(
id=f"ord-{int(time.time() * 1000)}",
customer_id=request.customer_id,
items=request.items,
total=total,
status=order_pb2.ORDER_STATUS_PENDING,
)
def ListOrders(self, request, context):
orders = get_orders_from_db(request.customer_id)
for order in orders:
yield order # server streaming
def serve():
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10)
)
order_pb2_grpc.add_OrderServiceServicer_to_server(
OrderServicer(), server
)
server.add_insecure_port("[::]:50051")
server.start()
print("gRPC server running on port 50051")
server.wait_for_termination()
if __name__ == "__main__":
serve()Client with Deadline
import grpc
import order_pb2
import order_pb2_grpc
channel = grpc.insecure_channel("localhost:50051")
stub = order_pb2_grpc.OrderServiceStub(channel)
# Unary call with 5-second deadline
try:
response = stub.CreateOrder(
order_pb2.CreateOrderRequest(
customer_id="cust-1",
items=[
order_pb2.LineItem(
product_id="p-1",
quantity=2,
unit_price=29.99,
)
],
),
timeout=5.0,
)
print(f"Created order: {response.id}")
except grpc.RpcError as e:
print(f"RPC failed: {e.code()} - {e.details()}")
# Server streaming
for order in stub.ListOrders(
order_pb2.ListOrdersRequest(customer_id="cust-1")
):
print(f"Order: {order.id}")9. Authentication and Security
gRPC provides channel-level encryption with TLS and call-level authentication via metadata. Production services should always use TLS.
TLS Configuration (Go Server)
import "google.golang.org/grpc/credentials"
creds, err := credentials.NewServerTLSFromFile(
"server.crt",
"server.key",
)
if err != nil {
log.Fatalf("failed to load TLS: %v", err)
}
srv := grpc.NewServer(grpc.Creds(creds))Token-Based Auth with Interceptors (Go)
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
func authInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(
codes.Unauthenticated, "missing metadata",
)
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(
codes.Unauthenticated, "missing token",
)
}
if err := validateToken(tokens[0]); err != nil {
return nil, status.Error(
codes.Unauthenticated, "invalid token",
)
}
return handler(ctx, req)
}
// Register the interceptor
srv := grpc.NewServer(
grpc.UnaryInterceptor(authInterceptor),
)Sending Auth Token from Client (Node.js)
const metadata = new grpc.Metadata();
metadata.set("authorization", "Bearer " + token);
client.createOrder(
{ customer_id: "cust-1", items: [] },
metadata,
(err, response) => {
if (err) console.error(err);
else console.log("Order:", response.id);
}
);10. Error Handling and Status Codes
gRPC defines a standard set of status codes. Always return meaningful codes and messages so clients can handle errors appropriately.
| Code | Name | Use Case |
|---|---|---|
| 0 | OK | Success |
| 1 | CANCELLED | Client cancelled the request |
| 3 | INVALID_ARGUMENT | Bad request data (validation) |
| 4 | DEADLINE_EXCEEDED | Request took too long |
| 5 | NOT_FOUND | Resource does not exist |
| 7 | PERMISSION_DENIED | Authenticated but not authorized |
| 13 | INTERNAL | Server-side bug |
| 14 | UNAVAILABLE | Service temporarily down (retry) |
| 16 | UNAUTHENTICATED | Missing or invalid credentials |
Rich Error Details (Go)
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
func (s *orderServer) GetOrder(
ctx context.Context,
req *pb.GetOrderRequest,
) (*pb.Order, error) {
order, err := s.db.FindOrder(req.Id)
if err != nil {
st := status.New(codes.NotFound, "order not found")
detail := &errdetails.ErrorInfo{
Reason: "ORDER_NOT_FOUND",
Domain: "order.myapp.com",
Metadata: map[string]string{
"order_id": req.Id,
},
}
st, _ = st.WithDetails(detail)
return nil, st.Err()
}
return order, nil
}Error Handling on the Client (Python)
try:
order = stub.GetOrder(
order_pb2.GetOrderRequest(id="ord-missing"),
timeout=5.0,
)
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.NOT_FOUND:
print("Order not found:", e.details())
elif e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
print("Request timed out")
elif e.code() == grpc.StatusCode.UNAVAILABLE:
print("Service down, retrying...")
else:
print(f"RPC error: {e.code()} - {e.details()}")11. gRPC-Web for Browser Clients
Browsers cannot make native gRPC calls due to missing HTTP/2 trailer support. gRPC-Web bridges this gap by translating browser-compatible requests into native gRPC through a proxy.
Architecture
Browser (gRPC-Web client)
|
| HTTP/1.1 or HTTP/2 (no trailers)
v
Envoy Proxy (or grpc-web middleware)
|
| Native gRPC over HTTP/2
v
gRPC Backend ServerEnvoy Proxy Configuration (Key Parts)
# envoy.yaml — essential gRPC-Web filters
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
http_filters:
- name: envoy.filters.http.grpc_web # key filter
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
clusters:
- name: grpc_service
type: logical_dns
http2_protocol_options: {} # must enable HTTP/2
load_assignment:
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: backend
port_value: 50051Browser Client (TypeScript)
import { OrderServiceClient } from "./gen/order_grpc_web_pb";
import { CreateOrderRequest, LineItem } from "./gen/order_pb";
const client = new OrderServiceClient(
"https://api.example.com:8080"
);
const item = new LineItem();
item.setProductId("p-1");
item.setQuantity(2);
item.setUnitPrice(29.99);
const request = new CreateOrderRequest();
request.setCustomerId("cust-1");
request.setItemsList([item]);
client.createOrder(request, {}, (err, response) => {
if (err) {
console.error("gRPC-Web error:", err.message);
return;
}
console.log("Order ID:", response.getId());
});12. Load Balancing and Service Mesh
gRPC uses long-lived HTTP/2 connections, which means a traditional L4 (TCP) load balancer will send all requests from one connection to the same backend. You need L7 (application-layer) balancing or client-side balancing.
Load Balancing Strategies
| Strategy | How It Works | Pros | Cons |
|---|---|---|---|
| L7 Proxy (Envoy) | Proxy understands HTTP/2 frames | Per-request balancing, no client changes | Extra hop latency |
| Client-side (round-robin) | Client discovers backends and distributes | No proxy needed, lowest latency | Client complexity, stale endpoints |
| Service Mesh (Istio) | Sidecar proxy per pod | Transparent, advanced features | Operational overhead, resource usage |
Kubernetes Headless Service
# Headless Service for client-side load balancing
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
clusterIP: None # returns all pod IPs for DNS
selector:
app: order-service
ports:
- port: 50051
targetPort: 50051
---
# Deployment with gRPC health probe
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
template:
spec:
containers:
- name: order-service
image: myapp/order-service:latest
ports:
- containerPort: 50051
readinessProbe:
grpc:
port: 5005113. Best Practices for Production
Always Set Deadlines
Never make a gRPC call without a deadline. Without deadlines, a slow or stuck server can hold client resources indefinitely, cascading failures across services.
// Go: always set a deadline
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second,
)
defer cancel()
// Node.js: set deadline in options
// const deadline = new Date();
// deadline.setSeconds(deadline.getSeconds() + 5);
// client.createOrder(request, { deadline }, callback);
// Python: timeout parameter
// stub.CreateOrder(request, timeout=5.0)Use Interceptors for Cross-Cutting Concerns
// Go: chain logging + auth + rate limit interceptors
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
loggingInterceptor, // logs method, duration, error
authInterceptor, // validates JWT token
rateLimitInterceptor, // token-bucket per client
),
)Health Checks and Reflection
import (
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
)
// Enable health checking
healthSrv := health.NewServer()
grpc_health_v1.RegisterHealthServer(srv, healthSrv)
healthSrv.SetServingStatus(
"order.OrderService",
grpc_health_v1.HealthCheckResponse_SERVING,
)
// Enable reflection (for grpcurl, grpcui)
reflection.Register(srv)
// Now you can introspect with grpcurl:
// grpcurl -plaintext localhost:50051 list
// grpcurl -plaintext localhost:50051 order.OrderService/CreateOrderProto File Best Practices
- Never reuse field numbers — deleted fields should be reserved
- Use packages — namespace your protos to avoid conflicts
- Prefix enum values — e.g.,
ORDER_STATUS_PENDINGnot justPENDING - Add a zero value for enums — first value should be
UNSPECIFIED = 0 - Use wrapper types for optional scalars —
google.protobuf.Int32Value - Version your APIs — use
package myapp.order.v1 - Lint your protos — use
buf lintto enforce style rules
Retry and Backoff
// Go: retry policy via service config JSON
serviceConfig := `{"methodConfig":[{"name":[{"service":"order.OrderService"}],
"retryPolicy":{"maxAttempts":3,"initialBackoff":"0.1s",
"maxBackoff":"1s","backoffMultiplier":2.0,
"retryableStatusCodes":["UNAVAILABLE","DEADLINE_EXCEEDED"]}}]}`
conn, _ := grpc.Dial("localhost:50051",
grpc.WithDefaultServiceConfig(serviceConfig),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)Graceful Shutdown
// Go: graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down gRPC server...")
srv.GracefulStop() // waits for in-flight RPCs
log.Println("Server stopped")- gRPC uses Protocol Buffers for binary serialization — 5-10x faster and smaller than JSON
- HTTP/2 multiplexing enables streaming and eliminates head-of-line blocking
- Define services once in .proto files, generate clients in any supported language
- Four communication patterns: unary, server streaming, client streaming, bidirectional
- Use gRPC-Web or a gateway proxy to expose gRPC services to browser clients
- Always set deadlines, use interceptors for cross-cutting concerns, and enable TLS in production
- gRPC integrates natively with Kubernetes, Istio, and Envoy for load balancing
- Use health checks, reflection, and structured status codes for production readiness