- Published on
gRPC & Protocol Buffers Complete Guide 2025: The New Standard for Microservices Communication
- Authors

- Name
- Youngju Kim
- @fjvbn20031
Table of Contents
1. REST vs gRPC: Why gRPC?
In microservices architecture, inter-service communication is a critical factor that determines system performance and reliability. REST APIs have long been the standard due to their simplicity and universality, but their limitations become apparent as the number of services increases.
1.1 Limitations of REST
There are three major issues with REST-based JSON communication.
First, serialization/deserialization overhead. JSON is text-based, making parsing expensive, and data sizes are 3-10x larger than binary formats. This difference is significant for internal service communication handling tens of thousands of requests per second.
Second, lack of schema enforcement. While you can document with OpenAPI/Swagger, there is no enforcement, and the contract between client and server is loose. When APIs change, errors are only discovered at runtime.
Third, HTTP/1.1 constraints. Head-of-Line Blocking, one request per connection, and no header compression make it unsuitable for high-performance communication.
1.2 Comparison Table
| Aspect | REST (JSON) | gRPC (Protobuf) |
|---|---|---|
| Protocol | HTTP/1.1 (mostly) | HTTP/2 |
| Data Format | JSON (text) | Protocol Buffers (binary) |
| Schema | Optional (OpenAPI) | Required (.proto) |
| Code Generation | Optional | Automatic (multi-language) |
| Streaming | Limited (SSE, WebSocket) | Native support (4 patterns) |
| Browser Support | Native | Requires gRPC-Web |
| Serialization Speed | Slow | Fast (10x) |
| Payload Size | Large | Small (3-10x) |
| Learning Curve | Low | Medium |
| Debugging | Easy (human-readable) | Difficult (binary) |
1.3 When to Use What
Choose REST when:
- Browser clients are the primary consumers
- Public API for external developers
- Simple CRUD operations
- Team lacks gRPC experience
Choose gRPC when:
- Internal microservice communication
- Real-time streaming is needed
- High performance and low latency are critical
- Supporting clients in multiple languages
- Strong type safety is required
2. Protocol Buffers Fundamentals
Protocol Buffers (Protobuf) is a language-neutral, platform-neutral binary serialization format developed by Google. It serves as the default IDL (Interface Definition Language) and serialization mechanism for gRPC.
2.1 Basic .proto File Structure
syntax = "proto3";
package ecommerce.v1;
option go_package = "github.com/mycompany/ecommerce/v1;ecommercev1";
option java_package = "com.mycompany.ecommerce.v1";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// Product service definition
service ProductService {
// Get a single product
rpc GetProduct(GetProductRequest) returns (Product);
// List products
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
// Create a product
rpc CreateProduct(CreateProductRequest) returns (Product);
// Update a product
rpc UpdateProduct(UpdateProductRequest) returns (Product);
// Delete a product
rpc DeleteProduct(DeleteProductRequest) returns (google.protobuf.Empty);
}
2.2 Scalar Types
message ScalarTypes {
// Numeric types
double price = 1; // 64-bit floating point
float rating = 2; // 32-bit floating point
int32 quantity = 3; // Variable-length encoding (inefficient for negatives)
int64 total_sales = 4; // Variable-length encoding
uint32 age = 5; // Unsigned 32-bit integer
uint64 view_count = 6; // Unsigned 64-bit integer
sint32 temperature = 7; // Efficient encoding for negatives
sint64 altitude = 8; // Efficient encoding for negatives
fixed32 hash = 9; // Always 4 bytes
fixed64 large_hash = 10; // Always 8 bytes
// String/bytes
string name = 11; // UTF-8 encoded string
bytes thumbnail = 12; // Arbitrary byte sequence
// Boolean
bool is_active = 13;
}
2.3 Message Types and Nesting
message Product {
string id = 1;
string name = 2;
string description = 3;
Money price = 4;
Category category = 5;
repeated string tags = 6; // Repeated field (array)
map<string, string> metadata = 7; // Map type
google.protobuf.Timestamp created_at = 8;
ProductStatus status = 9;
// Nested message
message Dimension {
double width = 1;
double height = 2;
double depth = 3;
string unit = 4;
}
Dimension dimension = 10;
}
message Money {
string currency_code = 1; // ISO 4217
int64 units = 2; // Integer part
int32 nanos = 3; // Fractional part in nanos
}
2.4 Enum Types
enum ProductStatus {
PRODUCT_STATUS_UNSPECIFIED = 0; // Always 0 for default
PRODUCT_STATUS_DRAFT = 1;
PRODUCT_STATUS_ACTIVE = 2;
PRODUCT_STATUS_ARCHIVED = 3;
PRODUCT_STATUS_DELETED = 4;
}
enum Category {
CATEGORY_UNSPECIFIED = 0;
CATEGORY_ELECTRONICS = 1;
CATEGORY_CLOTHING = 2;
CATEGORY_BOOKS = 3;
CATEGORY_FOOD = 4;
}
2.5 Oneof Types
message PaymentMethod {
string id = 1;
oneof method {
CreditCard credit_card = 2;
BankTransfer bank_transfer = 3;
DigitalWallet digital_wallet = 4;
}
}
message CreditCard {
string card_number_masked = 1;
string expiry_month = 2;
string expiry_year = 3;
string brand = 4;
}
message BankTransfer {
string bank_name = 1;
string account_number_masked = 2;
string routing_number = 3;
}
message DigitalWallet {
string provider = 1; // "apple_pay", "google_pay"
string email = 2;
}
2.6 Field Numbers and Versioning
In Protocol Buffers, field numbers are the key element that identifies fields in the wire format.
Field number rules:
- 1-15: Encoded in 1 byte (assign to frequently used fields)
- 16-2047: Encoded in 2 bytes
- 19000-19999: Reserved range (cannot use)
Backward compatibility rules:
message Product {
string id = 1;
string name = 2;
// Deleted fields: reserve both number and name
reserved 3, 6 to 8;
reserved "old_price", "legacy_tag";
// Adding new fields is always safe
string description = 4;
Money price = 5;
// Add from field 9 onwards
string sku = 9;
}
Versioning strategies:
- Adding fields: Always safe (clients unaware of new fields ignore them)
- Removing fields: Reserve the number with reserved, then remove
- Changing field types: Never do this (add a new field instead)
- Package version separation:
ecommerce.v1,ecommerce.v2
3. Four Communication Patterns
gRPC supports four communication patterns built on HTTP/2.
3.1 Unary RPC
The most basic pattern where the client sends one request and the server returns one response.
service UserService {
rpc GetUser(GetUserRequest) returns (User);
}
message GetUserRequest {
string user_id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
Go server implementation:
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
user, err := s.repo.FindByID(ctx, req.UserId)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "user %s not found", req.UserId)
}
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
}
return toProtoUser(user), nil
}
Node.js client:
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDefinition);
const client = new proto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary call
client.getUser({ user_id: 'user-123' }, (err, response) => {
if (err) {
console.error('Error:', err.message);
return;
}
console.log('User:', response);
});
3.2 Server Streaming RPC
The client sends one request and the server returns a stream of multiple messages.
service OrderService {
// Real-time order status tracking
rpc TrackOrder(TrackOrderRequest) returns (stream OrderStatus);
}
message TrackOrderRequest {
string order_id = 1;
}
message OrderStatus {
string order_id = 1;
string status = 2;
string location = 3;
google.protobuf.Timestamp updated_at = 4;
}
Go server implementation:
func (s *orderServer) TrackOrder(req *pb.TrackOrderRequest, stream pb.OrderService_TrackOrderServer) error {
orderID := req.OrderId
// Subscribe to event channel
events := s.eventBus.Subscribe(orderID)
defer s.eventBus.Unsubscribe(orderID, events)
for {
select {
case event := <-events:
status := &pb.OrderStatus{
OrderId: orderID,
Status: event.Status,
Location: event.Location,
UpdatedAt: timestamppb.Now(),
}
if err := stream.Send(status); err != nil {
return err
}
if event.Status == "delivered" {
return nil
}
case <-stream.Context().Done():
return stream.Context().Err()
}
}
}
3.3 Client Streaming RPC
The client sends a stream of multiple messages and the server returns one response.
service UploadService {
// File upload
rpc UploadFile(stream FileChunk) returns (UploadResponse);
}
message FileChunk {
string filename = 1;
bytes content = 2;
int64 offset = 3;
}
message UploadResponse {
string file_id = 1;
int64 total_size = 2;
string checksum = 3;
}
Go server implementation:
func (s *uploadServer) UploadFile(stream pb.UploadService_UploadFileServer) error {
var totalSize int64
var filename string
buffer := bytes.Buffer{}
for {
chunk, err := stream.Recv()
if err == io.EOF {
// All chunks received
fileID, checksum := s.storage.Save(filename, buffer.Bytes())
return stream.SendAndClose(&pb.UploadResponse{
FileId: fileID,
TotalSize: totalSize,
Checksum: checksum,
})
}
if err != nil {
return status.Errorf(codes.Internal, "failed to receive chunk: %v", err)
}
filename = chunk.Filename
totalSize += int64(len(chunk.Content))
buffer.Write(chunk.Content)
}
}
3.4 Bidirectional Streaming RPC
Client and server simultaneously exchange messages. Ideal for real-time chat, games, and stock tickers.
service ChatService {
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user_id = 1;
string room_id = 2;
string content = 3;
google.protobuf.Timestamp sent_at = 4;
MessageType type = 5;
}
enum MessageType {
MESSAGE_TYPE_UNSPECIFIED = 0;
MESSAGE_TYPE_TEXT = 1;
MESSAGE_TYPE_IMAGE = 2;
MESSAGE_TYPE_SYSTEM = 3;
}
Go server implementation:
func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// Broadcast to all clients in the same room
s.mu.RLock()
clients := s.rooms[msg.RoomId]
s.mu.RUnlock()
for _, client := range clients {
if err := client.Send(msg); err != nil {
log.Printf("failed to send to client: %v", err)
}
}
}
}
4. HTTP/2 Multiplexing
gRPC is built on HTTP/2, leveraging its key features.
4.1 Core HTTP/2 Features
Multiplexing: Handles multiple requests/responses simultaneously over a single TCP connection. Solves the HTTP/1.1 Head-of-Line Blocking problem.
Header Compression (HPACK): Eliminates duplicate headers and compresses with Huffman encoding. Significantly reduces header overhead on repeated requests.
Server Push: Server can send data without client request. Forms the basis of gRPC streaming.
Binary Framing: Unlike HTTP/1.1's text protocol, HTTP/2 communicates with binary frames.
HTTP/2 Connection
├── Stream 1: GetUser RPC
├── Stream 3: ListProducts RPC (concurrent)
├── Stream 5: TrackOrder RPC (server streaming)
└── Stream 7: Chat RPC (bidirectional streaming)
4.2 Connection Management
// Server configuration
server := grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
MaxConnectionIdle: 15 * time.Minute,
MaxConnectionAge: 30 * time.Minute,
MaxConnectionAgeGrace: 5 * time.Minute,
Time: 5 * time.Minute,
Timeout: 1 * time.Minute,
}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 5 * time.Second,
PermitWithoutStream: true,
}),
grpc.MaxRecvMsgSize(4 * 1024 * 1024), // 4MB
grpc.MaxSendMsgSize(4 * 1024 * 1024), // 4MB
)
5. Interceptors (Middleware)
Interceptors are gRPC middleware that execute common logic before and after requests.
5.1 Unary Interceptor
// Logging interceptor
func loggingUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// Log request
log.Printf("gRPC call: %s", info.FullMethod)
// Execute handler
resp, err := handler(ctx, req)
// Log response
duration := time.Since(start)
statusCode := status.Code(err)
log.Printf("gRPC response: method=%s duration=%v status=%s",
info.FullMethod, duration, statusCode)
return resp, err
}
// Authentication interceptor
func authUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Skip auth for public methods
if isPublicMethod(info.FullMethod) {
return handler(ctx, req)
}
// Extract token from metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "missing token")
}
// Validate token
claims, err := validateToken(tokens[0])
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
// Add user info to context
ctx = context.WithValue(ctx, userClaimsKey, claims)
return handler(ctx, req)
}
5.2 Stream Interceptor
func loggingStreamInterceptor(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
start := time.Now()
log.Printf("gRPC stream started: %s", info.FullMethod)
// Wrapped stream for message counting
wrapped := &wrappedStream{
ServerStream: ss,
recvCount: 0,
sendCount: 0,
}
err := handler(srv, wrapped)
log.Printf("gRPC stream ended: method=%s duration=%v recv=%d send=%d",
info.FullMethod, time.Since(start), wrapped.recvCount, wrapped.sendCount)
return err
}
type wrappedStream struct {
grpc.ServerStream
recvCount int
sendCount int
}
func (w *wrappedStream) RecvMsg(m interface{}) error {
err := w.ServerStream.RecvMsg(m)
if err == nil {
w.recvCount++
}
return err
}
func (w *wrappedStream) SendMsg(m interface{}) error {
err := w.ServerStream.SendMsg(m)
if err == nil {
w.sendCount++
}
return err
}
5.3 Interceptor Chain
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
recoveryUnaryInterceptor, // Panic recovery (outermost)
loggingUnaryInterceptor, // Logging
metricsUnaryInterceptor, // Metrics collection
authUnaryInterceptor, // Authentication
rateLimitUnaryInterceptor, // Rate limiting
validationUnaryInterceptor, // Input validation (innermost)
),
grpc.ChainStreamInterceptor(
recoveryStreamInterceptor,
loggingStreamInterceptor,
authStreamInterceptor,
),
)
6. Error Handling and Status Codes
6.1 gRPC Status Codes
| Code | Name | Description | HTTP Mapping |
|---|---|---|---|
| 0 | OK | Success | 200 |
| 1 | CANCELLED | Request cancelled | 499 |
| 2 | UNKNOWN | Unknown error | 500 |
| 3 | INVALID_ARGUMENT | Invalid argument | 400 |
| 4 | DEADLINE_EXCEEDED | Timeout | 504 |
| 5 | NOT_FOUND | Resource not found | 404 |
| 6 | ALREADY_EXISTS | Already exists | 409 |
| 7 | PERMISSION_DENIED | Insufficient permissions | 403 |
| 8 | RESOURCE_EXHAUSTED | Resource exhausted | 429 |
| 13 | INTERNAL | Internal server error | 500 |
| 14 | UNAVAILABLE | Service unavailable | 503 |
| 16 | UNAUTHENTICATED | Authentication failure | 401 |
6.2 Rich Error Responses
import (
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/status"
)
func (s *productServer) CreateProduct(ctx context.Context, req *pb.CreateProductRequest) (*pb.Product, error) {
// Input validation
violations := validateCreateProduct(req)
if len(violations) > 0 {
st := status.New(codes.InvalidArgument, "invalid product data")
br := &errdetails.BadRequest{}
for _, v := range violations {
br.FieldViolations = append(br.FieldViolations, &errdetails.BadRequest_FieldViolation{
Field: v.Field,
Description: v.Description,
})
}
st, err := st.WithDetails(br)
if err != nil {
return nil, status.Errorf(codes.Internal, "unexpected error: %v", err)
}
return nil, st.Err()
}
// Business logic
product, err := s.repo.Create(ctx, req)
if err != nil {
if isDuplicate(err) {
st := status.New(codes.AlreadyExists, "product already exists")
ri := &errdetails.ResourceInfo{
ResourceType: "Product",
ResourceName: req.Sku,
Description: "A product with this SKU already exists",
}
st, _ = st.WithDetails(ri)
return nil, st.Err()
}
return nil, status.Errorf(codes.Internal, "failed to create product: %v", err)
}
return product, nil
}
7. Deadline and Timeout
7.1 Deadline Propagation
// Client: set deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetProduct(ctx, &pb.GetProductRequest{ProductId: "prod-123"})
if err != nil {
st := status.Convert(err)
if st.Code() == codes.DeadlineExceeded {
log.Println("Request timed out")
}
}
// Server: check deadline
func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {
// Check deadline
deadline, ok := ctx.Deadline()
if ok {
remaining := time.Until(deadline)
if remaining < 100*time.Millisecond {
return nil, status.Errorf(codes.DeadlineExceeded, "insufficient time remaining")
}
}
// Propagate deadline with remaining time for downstream calls
product, err := s.repo.FindByID(ctx, req.ProductId)
if err != nil {
return nil, err
}
// Allocate 80% of remaining time for price service
remaining := time.Until(deadline)
priceCtx, cancel := context.WithTimeout(ctx, remaining*80/100)
defer cancel()
price, err := s.priceClient.GetPrice(priceCtx, &pb.GetPriceRequest{
ProductId: req.ProductId,
})
if err != nil {
// Fall back to cached price on failure
price = s.priceCache.Get(req.ProductId)
}
product.Price = price
return product, nil
}
8. Load Balancing
8.1 Client-Side Load Balancing
import (
"google.golang.org/grpc"
"google.golang.org/grpc/resolver"
_ "google.golang.org/grpc/balancer/roundrobin"
)
// Register custom resolver
resolver.Register(&myResolver{})
conn, err := grpc.Dial(
"my-service:///product-service",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
8.2 Proxy-Based Load Balancing (Envoy)
# envoy.yaml
static_resources:
listeners:
- name: grpc_listener
address:
socket_address:
address: 0.0.0.0
port_value: 9090
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: grpc
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: grpc_service
domains: ["*"]
routes:
- match:
prefix: "/"
grpc: {}
route:
cluster: grpc_backend
timeout: 30s
retry_policy:
retry_on: "unavailable,resource-exhausted"
num_retries: 3
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: grpc_backend
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
load_assignment:
cluster_name: grpc_backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: product-service
port_value: 50051
9. gRPC-Web: Using gRPC in Browsers
9.1 Why gRPC-Web Is Needed
Browsers cannot directly control HTTP/2 framing, so they cannot natively use gRPC. gRPC-Web is a proxy layer that bridges this gap.
9.2 Using Envoy as a Proxy
# envoy-grpc-web.yaml
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
9.3 TypeScript Client (Connect-Web)
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { createClient } from "@connectrpc/connect";
import { ProductService } from "./gen/product_connect";
const transport = createGrpcWebTransport({
baseUrl: "https://api.example.com",
});
const client = createClient(ProductService, transport);
// Unary call
async function getProduct(id: string) {
try {
const product = await client.getProduct({ productId: id });
console.log("Product:", product.name, product.price);
} catch (err) {
if (err instanceof ConnectError) {
console.error("gRPC Error:", err.code, err.message);
}
}
}
// Server Streaming
async function trackOrder(orderId: string) {
for await (const status of client.trackOrder({ orderId })) {
console.log("Order status:", status.status, status.location);
updateUI(status);
}
}
10. Reflection and Health Checking
10.1 Server Reflection
import "google.golang.org/grpc/reflection"
func main() {
server := grpc.NewServer()
pb.RegisterProductServiceServer(server, &productServer{})
// Enable reflection (for development)
reflection.Register(server)
lis, _ := net.Listen("tcp", ":50051")
server.Serve(lis)
}
10.2 Health Checking
import "google.golang.org/grpc/health"
import healthpb "google.golang.org/grpc/health/grpc_health_v1"
func main() {
server := grpc.NewServer()
// Register health check service
healthServer := health.NewServer()
healthpb.RegisterHealthServer(server, healthServer)
// Set service status
healthServer.SetServingStatus("product.v1.ProductService", healthpb.HealthCheckResponse_SERVING)
// Periodic health check
go func() {
for {
if dbHealthy() {
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
} else {
healthServer.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING)
}
time.Sleep(10 * time.Second)
}
}()
}
10.3 Kubernetes Health Probes
# kubernetes deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
spec:
template:
spec:
containers:
- name: product-service
image: product-service:latest
ports:
- containerPort: 50051
name: grpc
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
periodSeconds: 5
11. Service Mesh Integration
11.1 Istio and gRPC
# istio virtual service
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: product-service
spec:
hosts:
- product-service
http:
- match:
- headers:
content-type:
exact: application/grpc
route:
- destination:
host: product-service
subset: v2
weight: 90
- destination:
host: product-service
subset: v1
weight: 10
retries:
attempts: 3
perTryTimeout: 2s
retryOn: "unavailable,resource-exhausted"
timeout: 10s
11.2 Linkerd and gRPC
Linkerd natively supports HTTP/2, so it automatically recognizes gRPC traffic.
# linkerd service profile
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
name: product-service.default.svc.cluster.local
spec:
routes:
- name: GetProduct
condition:
method: POST
pathRegex: /ecommerce.v1.ProductService/GetProduct
responseClasses:
- condition:
status:
min: 200
max: 299
- name: ListProducts
condition:
method: POST
pathRegex: /ecommerce.v1.ProductService/ListProducts
isRetryable: true
12. Testing
12.1 Testing with grpcurl
# List services (requires reflection)
grpcurl -plaintext localhost:50051 list
# List service methods
grpcurl -plaintext localhost:50051 list ecommerce.v1.ProductService
# Describe a method
grpcurl -plaintext localhost:50051 describe ecommerce.v1.ProductService.GetProduct
# Unary call
grpcurl -plaintext \
-d '{"product_id": "prod-123"}' \
localhost:50051 ecommerce.v1.ProductService/GetProduct
# Call with headers
grpcurl -plaintext \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-d '{"name": "New Product", "price": {"currency_code": "USD", "units": 29, "nanos": 990000000}}' \
localhost:50051 ecommerce.v1.ProductService/CreateProduct
12.2 Integration Testing in Go
func TestGetProduct(t *testing.T) {
// In-memory gRPC server
lis := bufconn.Listen(1024 * 1024)
server := grpc.NewServer()
pb.RegisterProductServiceServer(server, newTestProductServer())
go func() {
if err := server.Serve(lis); err != nil {
t.Fatal(err)
}
}()
defer server.Stop()
// Client connection
conn, err := grpc.DialContext(context.Background(), "bufnet",
grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)
defer conn.Close()
client := pb.NewProductServiceClient(conn)
// Execute test
resp, err := client.GetProduct(context.Background(), &pb.GetProductRequest{
ProductId: "test-product-1",
})
require.NoError(t, err)
assert.Equal(t, "test-product-1", resp.Id)
assert.Equal(t, "Test Product", resp.Name)
}
12.3 evans Interactive Client
# evans REPL mode
evans --host localhost --port 50051 -r repl
# Select package
> package ecommerce.v1
# Select service
> service ProductService
# Call RPC
> call GetProduct
product_id (TYPE_STRING) => prod-123
# Result displayed as JSON
13. Performance Benchmarks
13.1 Protobuf vs JSON Performance Comparison
func BenchmarkProtobufMarshal(b *testing.B) {
product := createSampleProduct()
b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Marshal(product)
}
}
func BenchmarkJSONMarshal(b *testing.B) {
product := createSampleProductJSON()
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(product)
}
}
Typical benchmark results:
| Metric | Protobuf | JSON | Ratio |
|---|---|---|---|
| Serialization Speed | 150ns | 1500ns | 10x faster |
| Deserialization Speed | 200ns | 2000ns | 10x faster |
| Data Size | 68 bytes | 220 bytes | 3.2x smaller |
| Memory Allocations | 1 alloc | 8 allocs | 8x fewer |
13.2 gRPC vs REST Throughput
| Scenario | gRPC (req/s) | REST (req/s) | gRPC Advantage |
|---|---|---|---|
| Small payload (100B) | 45,000 | 15,000 | 3x |
| Medium payload (1KB) | 35,000 | 10,000 | 3.5x |
| Large payload (100KB) | 8,000 | 2,500 | 3.2x |
| Streaming (100K messages) | 150,000/s | N/A | - |
14. Real-World Microservices Implementation
14.1 Project Structure
ecommerce-grpc/
├── proto/
│ ├── buf.yaml
│ ├── buf.gen.yaml
│ └── ecommerce/
│ └── v1/
│ ├── product.proto
│ ├── order.proto
│ ├── payment.proto
│ └── common.proto
├── services/
│ ├── product/
│ │ ├── main.go
│ │ ├── server.go
│ │ ├── repository.go
│ │ └── server_test.go
│ ├── order/
│ │ ├── main.go
│ │ ├── server.go
│ │ └── saga.go
│ └── payment/
│ ├── main.go
│ └── server.go
├── gen/
│ └── ecommerce/
│ └── v1/
├── docker-compose.yaml
└── Makefile
14.2 Code Generation with Buf
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/protocolbuffers/go
out: gen
opt:
- paths=source_relative
- remote: buf.build/grpc/go
out: gen
opt:
- paths=source_relative
- require_unimplemented_servers=false
- remote: buf.build/connectrpc/go
out: gen
opt:
- paths=source_relative
# Generate code
buf generate proto
# Lint
buf lint proto
# Backward compatibility check
buf breaking proto --against '.git#branch=main'
15. Quiz
Test your understanding of gRPC and Protocol Buffers with the following quiz.
Q1: What is the biggest advantage of gRPC using HTTP/2?
Answer: Multiplexing
HTTP/2 multiplexing allows multiple gRPC calls to be processed simultaneously over a single TCP connection. In HTTP/1.1, only one request can be processed per connection causing Head-of-Line Blocking, but in HTTP/2, multiple streams operate independently. Additional benefits include header compression (HPACK), binary framing, and server push.
Q2: Why should you assign frequently used fields to field numbers 1-15 in Protocol Buffers?
Answer: Because the encoding size is smaller
Field numbers 1-15 are encoded in 1 byte, while 16-2047 are encoded in 2 bytes. Assigning lower numbers to frequently used fields reduces overall message size. This is especially beneficial for repeated fields that appear multiple times.
Q3: What is the difference between Bidirectional Streaming and Server Streaming?
Answer: Whether the client can also send messages as a stream
In Server Streaming, the client sends one request and the server responds with a stream. In Bidirectional Streaming, both client and server can simultaneously exchange messages through independent streams. Since the two streams are independent, the server does not need to wait for all client messages before responding.
Q4: Why are deadlines important in gRPC?
Answer: They propagate request timeouts in distributed systems to prevent resource waste
A deadline is the maximum wait time set by the client, and gRPC automatically propagates it to downstream service calls. When Service A calls Service B, and B calls C, if A's deadline has already passed, the work by B and C is meaningless. Deadline propagation prevents such unnecessary work.
Q5: Why is gRPC-Web necessary?
Answer: Because browsers cannot directly control HTTP/2 framing
Browser Fetch API and XMLHttpRequest do not support frame-level control of HTTP/2. gRPC uses HTTP/2 trailers to deliver status codes, which browsers cannot read. gRPC-Web translates the gRPC protocol into a browser-compatible format through a proxy like Envoy.
16. References
- gRPC Official Documentation - Official gRPC guides and tutorials
- Protocol Buffers Language Guide - Protobuf proto3 syntax guide
- gRPC Go Tutorial - Go language gRPC tutorial
- Buf - Protobuf Tools - Modern Protobuf management tool
- gRPC-Web GitHub - Official gRPC-Web repository
- Connect-RPC - Browser-compatible gRPC framework
- Envoy gRPC Configuration - Envoy's gRPC support
- grpcurl GitHub - gRPC command-line tool
- evans GitHub - gRPC interactive client
- Istio gRPC Traffic Management - Istio and gRPC integration
- gRPC Health Checking Protocol - Health check protocol specification
- Google API Design Guide - Google's API design guide
- gRPC Performance Best Practices - gRPC performance optimization guide