Skip to main content

gRPC Fundamentals and Best Practices

Created: February 21, 2026 Larry Qu 8 min read

gRPC is a high-performance, open-source framework for inter-service communication. It uses HTTP/2 for transport and Protocol Buffers as the interface definition language, offering significant advantages over traditional REST APIs.

gRPC uses HTTP/2 for transport and Protocol Buffers for interface definition, offering significant performance advantages over traditional REST APIs for inter-service communication.

What is gRPC?

gRPC (Google Remote Procedure Call) enables client applications to call methods on server applications on different machines as if it were a local object.

┌─────────────────────────────────────────────────────────────┐
│                    gRPC Architecture                         │
│                                                             │
│   ┌─────────────┐           ┌─────────────┐               │
│   │   Client    │           │   Server     │               │
│   │  Application│           │  Application │               │
│   └──────┬──────┘           └──────┬──────┘               │
│          │                         │                        │
│          │    gRPC Service         │                        │
│          │◄────────────────────────│                        │
│          │    (HTTP/2 +           │                        │
│          │     Protobuf)          │                        │
│          │                         │                        │
│   ┌──────▼──────┐           ┌──────▼──────┐               │
│   │ gRPC Client │           │gRPC Server  │               │
│   │   Stub      │           │  Handler    │               │
│   └─────────────┘           └─────────────┘               │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐  │
│   │              Protocol Buffers (Schema)              │  │
│   └─────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Why gRPC?

# Performance Comparison

performance_benefits = {
    "serialization": {
        "rest_json": "JSON parsing overhead",
        "grpc_protobuf": "Compact binary format - 10x smaller"
    },
    "transport": {
        "rest_http1": "Multiple connections, blocking",
        "grpc_http2": "Multiplexing, header compression"
    },
    "speed": {
        "rest": "Typical 50-100ms",
        "grpc": "Typical 5-15ms"
    },
    "streaming": {
        "rest": "Polling or WebSockets",
        "grpc": "Native bidirectional streaming"
    }
}

Protocol Buffers

Protocol Buffers (Protobuf) is Google’s language-agnostic, platform-neutral mechanism for serializing structured data.

Defining Messages

// user.proto

syntax = "proto3";

package user;

// Basic message
message User {
    string id = 1;
    string name = 2;
    string email = 3;
    int32 age = 4;
    bool active = 5;
    repeated string roles = 6;
    map<string, string> metadata = 7;
    CreatedAt created_at = 8;
}

// Nested message
message CreatedAt {
    int64 timestamp = 1;
    string timezone = 2;
}

// Enums
enum UserStatus {
    USER_STATUS_UNSPECIFIED = 0;
    USER_STATUS_ACTIVE = 1;
    USER_STATUS_INACTIVE = 2;
    USER_STATUS_SUSPENDED = 3;
}

Scalar Types

// Scalar types in Protobuf
message ScalarTypes {
    double   double_field = 1;   // 64-bit float
    float    float_field = 2;     // 32-bit float
    int32    int32_field = 3;    // Variable-length int
    int64    int64_field = 4;    // Variable-length int
    uint32   uint32_field = 5;   // Unsigned int32
    uint64   uint64_field = 6;   // Unsigned int64
    sint32   sint32_field = 7;   // Signed int32
    sint64   sint64_field = 8;   // Signed int64
    fixed32  fixed32_field = 9;   // Fixed 32-bit
    fixed64  fixed64_field = 10; // Fixed 64-bit
    bool     bool_field = 11;     // Boolean
    string   string_field = 12;   // UTF-8 string
    bytes    bytes_field = 13;    // Byte string
}

Oneof and Well-Known Types

// Oneof - when only one field should be set
message Response {
    oneof result {
        User user = 1;
        Order order = 2;
        Error error = 3;
    }
}

// Well-known types
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/empty.proto";

message Event {
    string id = 1;
    google.protobuf.Timestamp created_at = 2;
    google.protobuf.Duration duration = 3;
    google.protobuf.StringValue name = 4;  // Nullable string
    google.protobuf.Int32Value count = 5;  // Nullable int
    google.protobuf.Empty status = 6;       // Empty message
}

gRPC Service Definitions

Unary RPC (Request-Response)

// Simple request-response
service UserService {
    rpc GetUser (GetUserRequest) returns (User);
    rpc CreateUser (CreateUserRequest) returns (User);
    rpc UpdateUser (UpdateUserRequest) returns (User);
    rpc DeleteUser (DeleteUserRequest) returns (Empty);
}

message GetUserRequest {
    string user_id = 1;
}

message CreateUserRequest {
    string name = 1;
    string email = 2;
    int32 age = 3;
}

message UpdateUserRequest {
    string user_id = 1;
    string name = 2;
    string email = 3;
}

message DeleteUserRequest {
    string user_id = 1;
}

message Empty {}

Server Streaming

// Server streams responses
service OrderService {
    rpc GetOrders(GetOrdersRequest) returns (stream Order);
    rpc StreamOrderUpdates(StreamRequest) returns (stream OrderUpdate);
}

message GetOrdersRequest {
    string user_id = 1;
    int32 limit = 2;
}

message Order {
    string order_id = 1;
    string user_id = 2;
    repeated OrderItem items = 3;
    double total = 4;
    OrderStatus status = 5;
}

message OrderItem {
    string product_id = 1;
    string name = 2;
    int32 quantity = 3;
    double price = 4;
}

enum OrderStatus {
    ORDER_STATUS_UNSPECIFIED = 0;
    ORDER_STATUS_PENDING = 1;
    ORDER_STATUS_PAID = 2;
    ORDER_STATUS_SHIPPED = 3;
    ORDER_STATUS_DELIVERED = 4;
}

Client Streaming

// Client streams requests
service UploadService {
    rpc UploadChunks(stream Chunk) returns (UploadResult);
    rpc ProcessBatch(stream ProcessRequest) returns (ProcessResponse);
}

message Chunk {
    string upload_id = 1;
    int32 sequence = 2;
    bytes data = 3;
}

message UploadResult {
    string file_id = 1;
    int64 size = 2;
    bool success = 3;
}

message ProcessRequest {
    string process_id = 1;
    RequestData data = 2;
}

message ProcessResponse {
    string process_id = 1;
    int32 processed_count = 2;
    repeated Error errors = 3;
}

Bidirectional Streaming

// Both client and server stream
service ChatService {
    rpc Chat(stream ChatMessage) returns (stream ChatMessage);
    rpc Monitor(stream MonitorRequest) returns (stream MonitorResponse);
}

message ChatMessage {
    string session_id = 1;
    string user_id = 2;
    string message = 3;
    int64 timestamp = 4;
}

message MonitorRequest {
    string service_id = 1;
    MetricType metric = 2;
}

message MonitorResponse {
    string service_id = 1;
    double cpu_usage = 2;
    double memory_usage = 3;
    int64 request_count = 4;
}

enum MetricType {
    METRIC_TYPE_UNSPECIFIED = 0;
    METRIC_TYPE_CPU = 1;
    METRIC_TYPE_MEMORY = 2;
    METRIC_TYPE_REQUESTS = 3;
}

Python gRPC Implementation

Server Implementation

# user_service.py
import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc

class UserServiceServicer(user_pb2_grpc.UserServiceServicer):
    
    def GetUser(self, request, context):
        user = database.get_user(request.user_id)
        if not user:
            context.set_code(grpc.StatusCode.NOT_FOUND)
            context.set_details("User not found")
            return user_pb2.User()
        
        return user_pb2.User(
            id=user.id,
            name=user.name,
            email=user.email,
            age=user.age,
            active=user.active
        )
    
    def CreateUser(self, request, context):
        user = User(
            name=request.name,
            email=request.email,
            age=request.age
        )
        saved_user = database.save_user(user)
        
        return user_pb2.User(
            id=saved_user.id,
            name=saved_user.name,
            email=saved_user.email,
            age=saved_user.age,
            active=True
        )
    
    def GetOrders(self, request, context):
        orders = database.get_orders(request.user_id, request.limit)
        
        for order in orders:
            yield user_pb2.Order(
                id=order.id,
                user_id=order.user_id,
                total=order.total,
                status=order.status
            )

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    user_pb2_grpc.add_UserServiceServicer_to_server(
        UserServiceServicer(), server
    )
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

Client Implementation

# user_client.py
import grpc
import user_pb2
import user_pb2_grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = user_pb2_grpc.UserServiceStub(channel)
    
    # Unary call
    response = stub.GetUser(user_pb2.GetUserRequest(user_id="123"))
    print(f"User: {response.name}, {response.email}")
    
    # Create user
    new_user = stub.CreateUser(user_pb2.CreateUserRequest(
        name="John Doe",
        email="[email protected]",
        age=30
    ))
    print(f"Created user: {new_user.id}")
    
    # Server streaming
    orders = stub.GetOrders(user_pb2.GetOrdersRequest(
        user_id="123",
        limit=10
    ))
    for order in orders:
        print(f"Order: {order.id}, Total: {order.total}")

if __name__ == '__main__':
    run()

Bidirectional Streaming Example

# chat_client.py
import grpc
import chat_pb2
import chat_pb2_grpc
import threading
import time

class ChatClient:
    def __init__(self):
        self.channel = grpc.insecure_channel('localhost:50051')
        self.stub = chat_pb2_grpc.ChatServiceStub(self.channel)
    
    def send_messages(self):
        def message_generator():
            for i in range(5):
                yield chat_pb2.ChatMessage(
                    session_id="session-1",
                    user_id="user-123",
                    message=f"Message {i}",
                    timestamp=int(time.time())
                )
                time.sleep(1)
        
        responses = self.stub.Chat(message_generator())
        for response in responses:
            print(f"Server: {response.message}")
    
    def receive_messages(self):
        # Handle incoming messages
        pass

# Server-side bidirectional streaming
class ChatServiceServicer(chat_pb2_grpc.ChatServiceServicer):
    
    async def Chat(self, request_iterator, context):
        async for message in request_iterator:
            # Process message
            response = chat_pb2.ChatMessage(
                session_id=message.session_id,
                user_id="server",
                message=f"Echo: {message.message}",
                timestamp=int(time.time())
            )
            yield response

Best Practices

Schema Design

// Good practices

// 1. Use clear naming conventions
message UserProfile {  // Not UserProfileData
    // ...
}

// 2. Use appropriate field numbers
message GoodMessage {
    int32 id = 1;           // First field = 1
    string name = 2;        // Second = 2
    // Don't skip numbers unnecessarily
}

// 3. Add comments
message User {
    // Unique identifier
    string id = 1;
    
    // User's full name
    string name = 2;
    
    // User's email address
    string email = 3;
}

// 4. Use enums for status fields
enum Status {
    STATUS_UNSPECIFIED = 0;  // Required for proto3
    STATUS_ACTIVE = 1;
    STATUS_INACTIVE = 2;
}

Error Handling

# gRPC Error Codes

error_handling = {
    "OK": "Success",
    "CANCELLED": "Operation cancelled",
    "UNKNOWN": "Unknown error",
    "INVALID_ARGUMENT": "Client provided invalid argument",
    "DEADLINE_EXCEEDED": "Operation timed out",
    "NOT_FOUND": "Resource not found",
    "ALREADY_EXISTS": "Resource already exists",
    "PERMISSION_DENIED": "No permission",
    "RESOURCE_EXHAUSTED": "Resource exhausted",
    "FAILED_PRECONDITION": "Precondition failed",
    "ABORTED": "Operation aborted",
    "OUT_OF_RANGE": "Out of range",
    "UNIMPLEMENTED": "Operation not implemented",
    "INTERNAL": "Internal error",
    "UNAVAILABLE": "Service unavailable",
    "DATA_LOSS": "Data loss"
}

# Raising errors in Python
def GetUser(self, request, context):
    user = database.get_user(request.user_id)
    if not user:
        context.set_code(grpc.StatusCode.NOT_FOUND)
        context.set_details("User not found")
        return user_pb2.User()
    
    # Use trailing metadata for additional info
    metadata = [('user_email', user.email)]
    context.send_initial_metadata(metadata)
    
    return user_pb2.User(...)

Connection Management

# Secure connection (TLS)
def create_secure_channel():
    # Server authentication
    credentials = grpc.ssl_channel_credentials(
        root_certificates=None,  # Use system certs
        private_key=None,
        certificate_chain=None
    )
    channel = grpc.secure_channel(
        'server.example.com:443',
        credentials
    )
    
    # Mutual TLS
    with open('client.key', 'rb') as f:
        private_key = f.read()
    with open('client.crt', 'rb') as f:
        certificate_chain = f.read()
    
    credentials = grpc.ssl_channel_credentials(
        root_certificates=open('ca.crt', 'rb').read(),
        private_key=private_key,
        certificate_chain=certificate_chain
    )

# Authentication with tokens
def create_authenticated_channel():
    credentials = grpc.access_token_call_credentials(
        get_access_token()
    )
    composite = grpc.composite_channel_credentials(
        grpc.ssl_channel_credentials(),
        credentials
    )
    return grpc.secure_channel(
        'server.example.com:443',
        composite
    )

# Keep-alive
channel = grpc.insecure_channel(
    'localhost:50051',
    options=[
        ('grpc.keepalive_time_ms', 10000),
        ('grpc.keepalive_timeout_ms', 5000),
        ('grpc.keepalive_permit_without_calls', True),
    ]
)

Performance Optimization

# Connection pooling
channel_pool = grpc.pool(
    lambda: grpc.insecure_channel('localhost:50051'),
    max_size=10,
    max_workers=5
)

# Compression
stub = user_pb2_grpc.UserServiceStub(
    grpc.intercept_channel(
        channel,
        grpc.compression_algorithm(grpc.Compression.Gzip)
    )
)

# Disable retry for non-idempotent methods
stub = user_pb2_grpc.UserServiceStub(
    channel,
    options=[
        ('grpc.enable_retries', 0)
    ]
)

# Set timeout
try:
    response = stub.GetUser(
        request,
        timeout=5.0,
        metadata=[('authorization', f'Bearer {token}')]
    )
except grpc.RpcError as e:
    if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
        print("Request timed out")

Migration from REST

# REST to gRPC Migration Strategy

migration_plan = """
1. Start with new services using gRPC
2. Create gRPC gateway for REST compatibility
3. Migrate high-traffic endpoints first
4. Use proto3 for all new services
5. Implement interceptors for logging/monitoring
"""

# gRPC Gateway for REST compatibility
# Allows REST clients to access gRPC services

from grpc_gateway import serve_grpc_gateway

# gateway.py
class Gateway:
    def __init__(self, grpc_server):
        self.grpc_server = grpc_server
    
    @app.route('/api/v1/users/<user_id>', methods=['GET'])
    def get_user(user_id):
        # Convert REST request to gRPC
        request = user_pb2.GetUserRequest(user_id=user_id)
        
        # Call gRPC service
        response = self.stub.GetUser(request)
        
        # Convert gRPC response to REST
        return jsonify({
            'id': response.id,
            'name': response.name,
            'email': response.email
        })

Conclusion

gRPC offers significant advantages for inter-service communication:

  • Performance: HTTP/2 + Protocol Buffers provides 10x performance improvement over REST/JSON
  • Streaming: Native support for bidirectional streaming
  • Type Safety: Schema validation at compile time
  • Code Generation: Auto-generate client/server code in multiple languages

Use gRPC when you need high performance, have a polyglot environment, or need streaming capabilities. Consider REST for public APIs or when HTTP caching is important.


Comments

Share this article

Scan to read on mobile