Using Protocol Buffers with gRPC

Build high-performance microservices with gRPC and Protobuf

Published: January 2025 • 15 min read

gRPC is THE killer use case for Protocol Buffers. Google built gRPC specifically to work with protobuf, and together they create incredibly fast, type-safe microservices. If you're building distributed systems, this is the modern way to do it.

This guide walks you through building a complete gRPC service with Protocol Buffers. We'll use a telecom subscriber service as our example, and show you how to build both the server and client. Simple, practical, and ready for production.

What is gRPC?

gRPC is a modern RPC (Remote Procedure Call) framework that makes calling methods on remote servers feel like calling local functions. Instead of dealing with HTTP endpoints and JSON parsing, you just call functions with type-safe parameters.

Why gRPC + Protobuf?

  • Fast: Binary protocol is 7-10x faster than REST/JSON
  • Type-Safe: Compiler checks types, catches errors early
  • Streaming: Built-in support for real-time bidirectional streaming
  • Multi-Language: One .proto file generates code for 11+ languages
  • HTTP/2: Multiplexing, header compression, connection reuse

Step 1: Define Your Service

First, we define our service in a .proto file. We'll create a simple subscriber management service.

// subscriber_service.proto
syntax = "proto3";

package telecom;

// The subscriber management service
service SubscriberService {
  // Get subscriber by phone number
  rpc GetSubscriber (GetSubscriberRequest) returns (Subscriber);
  
  // Create a new subscriber
  rpc CreateSubscriber (CreateSubscriberRequest) returns (Subscriber);
  
  // List all subscribers (server streaming)
  rpc ListSubscribers (ListSubscribersRequest) returns (stream Subscriber);
  
  // Update multiple subscribers (client streaming)
  rpc BatchUpdateSubscribers (stream Subscriber) returns (BatchUpdateResponse);
  
  // Real-time subscriber sync (bidirectional streaming)
  rpc SyncSubscribers (stream Subscriber) returns (stream Subscriber);
}

// Message definitions
message Subscriber {
  string msisdn = 1;
  string name = 2;
  string email = 3;
  SubscriptionType type = 4;
  bool is_active = 5;
}

enum SubscriptionType {
  PREPAID = 0;
  POSTPAID = 1;
  CORPORATE = 2;
}

message GetSubscriberRequest {
  string msisdn = 1;
}

message CreateSubscriberRequest {
  string msisdn = 1;
  string name = 2;
  string email = 3;
  SubscriptionType type = 4;
}

message ListSubscribersRequest {
  int32 page_size = 1;
  string page_token = 2;
}

message BatchUpdateResponse {
  int32 updated_count = 1;
}

Note: gRPC supports 4 types of RPC: Unary (simple request-response), Server streaming, Client streaming, and Bidirectional streaming. We'll show examples of each.

Step 2: Build a Python gRPC Server

Let's implement the server in Python. First, generate the code:

# Install dependencies
pip install grpcio grpcio-tools

# Generate Python code
python -m grpc_tools.protoc -I. \
  --python_out=. \
  --grpc_python_out=. \
  subscriber_service.proto

Now implement the server:

# server.py
import grpc
from concurrent import futures
import subscriber_service_pb2
import subscriber_service_pb2_grpc

class SubscriberService(subscriber_service_pb2_grpc.SubscriberServiceServicer):
    def __init__(self):
        # In-memory database (use real DB in production)
        self.subscribers = {}
    
    def GetSubscriber(self, request, context):
        """Simple unary RPC"""
        msisdn = request.msisdn
        
        if msisdn not in self.subscribers:
            context.set_code(grpc.StatusCode.NOT_FOUND)
            context.set_details(f'Subscriber {msisdn} not found')
            return subscriber_service_pb2.Subscriber()
        
        return self.subscribers[msisdn]
    
    def CreateSubscriber(self, request, context):
        """Create a new subscriber"""
        subscriber = subscriber_service_pb2.Subscriber(
            msisdn=request.msisdn,
            name=request.name,
            email=request.email,
            type=request.type,
            is_active=True
        )
        
        self.subscribers[request.msisdn] = subscriber
        print(f"Created subscriber: {request.msisdn}")
        
        return subscriber
    
    def ListSubscribers(self, request, context):
        """Server streaming - send multiple responses"""
        for msisdn, subscriber in self.subscribers.items():
            yield subscriber
            # Server can send data as it becomes available
    
    def BatchUpdateSubscribers(self, request_iterator, context):
        """Client streaming - receive multiple requests"""
        count = 0
        for subscriber in request_iterator:
            self.subscribers[subscriber.msisdn] = subscriber
            count += 1
        
        return subscriber_service_pb2.BatchUpdateResponse(
            updated_count=count
        )
    
    def SyncSubscribers(self, request_iterator, context):
        """Bidirectional streaming - real-time sync"""
        for subscriber in request_iterator:
            # Update local copy
            self.subscribers[subscriber.msisdn] = subscriber
            
            # Echo back confirmation
            yield subscriber

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    subscriber_service_pb2_grpc.add_SubscriberServiceServicer_to_server(
        SubscriberService(), server
    )
    
    server.add_insecure_port('[::]:50051')
    print("Server started on port 50051")
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

Step 3: Build a Python gRPC Client

Now let's create a client that calls our service:

# client.py
import grpc
import subscriber_service_pb2
import subscriber_service_pb2_grpc

def run():
    # Create channel to server
    channel = grpc.insecure_channel('localhost:50051')
    stub = subscriber_service_pb2_grpc.SubscriberServiceStub(channel)
    
    # 1. Unary RPC - Create subscriber
    print("\n=== Creating Subscriber ===")
    response = stub.CreateSubscriber(
        subscriber_service_pb2.CreateSubscriberRequest(
            msisdn="+91-9876543210",
            name="Telecom Customer",
            email="customer@telecom.com",
            type=subscriber_service_pb2.POSTPAID
        )
    )
    print(f"Created: {response.name} ({response.msisdn})")
    
    # 2. Unary RPC - Get subscriber
    print("\n=== Getting Subscriber ===")
    response = stub.GetSubscriber(
        subscriber_service_pb2.GetSubscriberRequest(
            msisdn="+91-9876543210"
        )
    )
    print(f"Found: {response.name} - {response.email}")
    
    # 3. Server streaming - List all subscribers
    print("\n=== Listing All Subscribers (Server Streaming) ===")
    responses = stub.ListSubscribers(
        subscriber_service_pb2.ListSubscribersRequest(page_size=100)
    )
    for subscriber in responses:
        print(f"  - {subscriber.name} ({subscriber.msisdn})")
    
    # 4. Client streaming - Batch update
    print("\n=== Batch Update (Client Streaming) ===")
    def generate_updates():
        # Send multiple updates in one call
        for i in range(3):
            yield subscriber_service_pb2.Subscriber(
                msisdn=f"+91-912345{i:04d}",
                name=f"Subscriber {i}",
                email=f"sub{i}@telecom.com",
                type=subscriber_service_pb2.PREPAID,
                is_active=True
            )
    
    response = stub.BatchUpdateSubscribers(generate_updates())
    print(f"Updated {response.updated_count} subscribers")
    
    # 5. Bidirectional streaming - Real-time sync
    print("\n=== Real-time Sync (Bidirectional Streaming) ===")
    def generate_sync_requests():
        for i in range(2):
            yield subscriber_service_pb2.Subscriber(
                msisdn=f"+91-918888{i:04d}",
                name=f"Sync Subscriber {i}",
                type=subscriber_service_pb2.CORPORATE,
                is_active=True
            )
    
    responses = stub.SyncSubscribers(generate_sync_requests())
    for response in responses:
        print(f"Synced: {response.name} ({response.msisdn})")

if __name__ == '__main__':
    run()

Run the server in one terminal (python server.py) and the client in another (python client.py).

Example: Go gRPC Server

Go is extremely popular for gRPC services. Here's a quick example:

// Install dependencies
go get google.golang.org/grpc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

// Generate Go code
protoc --go_out=. --go_opt=paths=source_relative \
  --go-grpc_out=. --go-grpc_opt=paths=source_relative \
  subscriber_service.proto
// server.go
package main

import (
    "context"
    "log"
    "net"
    
    pb "path/to/subscriber_service"
    "google.golang.org/grpc"
)

type server struct {
    pb.UnimplementedSubscriberServiceServer
    subscribers map[string]*pb.Subscriber
}

func (s *server) GetSubscriber(ctx context.Context, req *pb.GetSubscriberRequest) (*pb.Subscriber, error) {
    if sub, ok := s.subscribers[req.Msisdn]; ok {
        return sub, nil
    }
    return nil, grpc.Errorf(grpc.Code_NotFound, "subscriber not found")
}

func (s *server) CreateSubscriber(ctx context.Context, req *pb.CreateSubscriberRequest) (*pb.Subscriber, error) {
    sub := &pb.Subscriber{
        Msisdn:   req.Msisdn,
        Name:     req.Name,
        Email:    req.Email,
        Type:     req.Type,
        IsActive: true,
    }
    
    s.subscribers[req.Msisdn] = sub
    log.Printf("Created subscriber: %s", req.Msisdn)
    
    return sub, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    
    s := grpc.NewServer()
    pb.RegisterSubscriberServiceServer(s, &server{
        subscribers: make(map[string]*pb.Subscriber),
    })
    
    log.Printf("Server listening on :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

Example: Node.js gRPC Service

Node.js works great with gRPC too. Perfect for TypeScript microservices:

// Install dependencies
npm install @grpc/grpc-js @grpc/proto-loader

// server.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_PATH = './subscriber_service.proto';

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const proto = grpc.loadPackageDefinition(packageDefinition).telecom;

const subscribers = new Map();

function getSubscriber(call, callback) {
    const msisdn = call.request.msisdn;
    
    if (subscribers.has(msisdn)) {
        callback(null, subscribers.get(msisdn));
    } else {
        callback({
            code: grpc.status.NOT_FOUND,
            details: `Subscriber ${msisdn} not found`
        });
    }
}

function createSubscriber(call, callback) {
    const subscriber = {
        msisdn: call.request.msisdn,
        name: call.request.name,
        email: call.request.email,
        type: call.request.type,
        is_active: true
    };
    
    subscribers.set(call.request.msisdn, subscriber);
    console.log(`Created subscriber: ${call.request.msisdn}`);
    
    callback(null, subscriber);
}

function listSubscribers(call) {
    // Server streaming
    subscribers.forEach((subscriber) => {
        call.write(subscriber);
    });
    call.end();
}

const server = new grpc.Server();

server.addService(proto.SubscriberService.service, {
    getSubscriber,
    createSubscriber,
    listSubscribers
});

server.bindAsync(
    '0.0.0.0:50051',
    grpc.ServerCredentials.createInsecure(),
    () => {
        console.log('Server running on port 50051');
        server.start();
    }
);

Error Handling in gRPC

gRPC has standard error codes. Use them for consistent error handling:

# Python - Raising errors
def GetSubscriber(self, request, context):
    if not request.msisdn:
        context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
        context.set_details('MSISDN is required')
        return subscriber_service_pb2.Subscriber()
    
    if request.msisdn not in self.subscribers:
        context.set_code(grpc.StatusCode.NOT_FOUND)
        context.set_details(f'Subscriber {request.msisdn} not found')
        return subscriber_service_pb2.Subscriber()
    
    # ... return subscriber

# Python - Catching errors
try:
    response = stub.GetSubscriber(request)
except grpc.RpcError as e:
    print(f"Error: {e.code().name}")
    print(f"Details: {e.details()}")
    
    if e.code() == grpc.StatusCode.NOT_FOUND:
        print("Subscriber doesn't exist")
    elif e.code() == grpc.StatusCode.INVALID_ARGUMENT:
        print("Invalid request")

Common gRPC Status Codes

  • OK - Success
  • CANCELLED - Operation cancelled
  • INVALID_ARGUMENT - Bad request parameters
  • NOT_FOUND - Resource doesn't exist
  • ALREADY_EXISTS - Duplicate resource
  • PERMISSION_DENIED - Access denied
  • UNAUTHENTICATED - Not authenticated
  • INTERNAL - Server error
  • UNAVAILABLE - Service unavailable
  • DEADLINE_EXCEEDED - Timeout

gRPC Best Practices

✓ Use Deadlines/Timeouts

Always set timeouts to prevent hanging forever. gRPC calls should have deadlines.

# Python client with timeout
response = stub.GetSubscriber(
    request,
    timeout=5.0  # 5 seconds
)

// Go client with context deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
response, err := client.GetSubscriber(ctx, request)

✓ Reuse Channels

Creating channels is expensive. Create once, reuse across many calls.

# BAD - Creates new channel each time
def call_service():
    channel = grpc.insecure_channel('localhost:50051')
    stub = SubscriberServiceStub(channel)
    response = stub.GetSubscriber(request)
    channel.close()

# GOOD - Reuse channel
channel = grpc.insecure_channel('localhost:50051')
stub = SubscriberServiceStub(channel)

def call_service():
    response = stub.GetSubscriber(request)  # Reuses connection

✓ Use Streaming for Large Data

Don't send huge arrays in one message. Use streaming to send data incrementally.

# BAD - Loads everything into memory
def ListAllSubscribers(self, request, context):
    all_subs = load_million_subscribers()  # Memory explosion!
    return ListResponse(subscribers=all_subs)

# GOOD - Stream results
def ListAllSubscribers(self, request, context):
    for subscriber in iterate_subscribers():  # One at a time
        yield subscriber  # Client receives incrementally

✓ Add Health Checks

Use gRPC health checking protocol for load balancers and orchestrators.

# Python - Add health check service
from grpc_health.v1 import health
from grpc_health.v1 import health_pb2_grpc

health_servicer = health.HealthServicer()
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)

# Set service status
health_servicer.set("telecom.SubscriberService", health_pb2.HealthCheckResponse.SERVING)

Production Deployment

🔒 Use TLS in Production

Never use insecure channels in production. Always enable TLS.

# Python - Server with TLS
credentials = grpc.ssl_server_credentials([
    (private_key, certificate_chain)
])
server.add_secure_port('[::]:50051', credentials)

# Python - Client with TLS
credentials = grpc.ssl_channel_credentials(root_certificates)
channel = grpc.secure_channel('server:50051', credentials)

📊 Add Logging and Metrics

Monitor your gRPC services. Track latency, error rates, and throughput.

# Python - Add interceptors for logging
import logging

class LoggingInterceptor(grpc.ServerInterceptor):
    def intercept_service(self, continuation, handler_call_details):
        logging.info(f"Calling: {handler_call_details.method}")
        return continuation(handler_call_details)

server = grpc.server(
    futures.ThreadPoolExecutor(),
    interceptors=[LoggingInterceptor()]
)

⚖️ Use Load Balancing

gRPC supports client-side and server-side load balancing. Use service mesh (Istio, Linkerd) or gRPC's built-in LB for distributing traffic across instances.

Related Resources

External References

Official Documentation

Conclusion

gRPC + Protocol Buffers is the modern standard for microservices. It's fast, type-safe, and supports streaming out of the box. Once you try it, going back to REST/JSON feels slow and error-prone.

Start with simple unary RPCs, then explore streaming when you need real-time updates. Use TLS in production, add proper error handling, and monitor your services. gRPC makes building distributed systems significantly easier than traditional approaches.