gRPC and Protocol Buffers

Build high-performance RPC services with Protocol Buffers, streaming, and modern API patterns

gRPC is a modern, high-performance Remote Procedure Call (RPC) framework developed by Google. It uses HTTP/2 for transport, Protocol Buffers for serialization, and provides features like authentication, load balancing, and bidirectional streaming that make it ideal for microservices communication.

Why gRPC matters: When building distributed systems, performance and type safety are critical. gRPC's binary Protocol Buffers format is 60-70% smaller than JSON, leading to faster serialization and lower bandwidth usage. Its HTTP/2 foundation enables multiplexing, header compression, and native streaming support.

In this lesson, you'll learn Protocol Buffers syntax and data types, compare gRPC with REST and GraphQL, build your first gRPC service with Python, implement all four streaming patterns (unary, server, client, bidirectional), run performance benchmarks showing gRPC's 3-5x speed advantage, understand when to choose gRPC over REST, and implement production best practices including TLS, error handling, health checks, and monitoring.

Lesson Sections

Introduction to gRPC

gRPC (gRPC Remote Procedure Calls) is a modern RPC framework developed by Google. It allows client applications to directly call methods on server applications as if they were local objects. It uses HTTP/2 for transport, Protocol Buffers as the interface description language, and provides features like authentication, support for load balancing, and bidirectional streaming.

Currently, gRPC is used mainly for internal services which are not exposed directly to the world. That's why in case you need to consume a gRPC service from a web application or from a language not supported, gRPC offers a REST API Gateway (you will lose most of the benefits of gRPC, but if you need access from an existing service you could do it without re-implementing the service). The gRPC gateway plugin generates a full-fledged REST API server with a reverse proxy and Swagger documentation.

Key Benefits
  • High Performance: Binary serialization with Protobuf
  • HTTP/2: Multiplexing, streaming, header compression
  • Type Safety: Strongly-typed contracts with Protobuf
  • Code Generation: Auto-generate client/server code
  • Streaming: Bidirectional streaming support
  • Multi-Language: Supports 10+ programming languages
Limitations
  • Browser Support: Limited browser support (requires gRPC-Web)
  • Human-Readability: Binary format is not human-readable
  • Learning Curve: Requires Protobuf knowledge
  • Debugging: Harder to debug than JSON/REST
  • Internal Use: Primarily for service-to-service communication
Further Reading

For an in-depth comparison between gRPC, GraphQL and REST with practical Python examples, check out:

My Next API — gRPC | RESTful

gRPC Architecture

In gRPC, a client application can directly call methods on a server application on a different machine as if it were a local object, making it easier to create distributed applications and services.

gRPC Architecture

gRPC ClientPythongRPC ServerPythonProtobuf StubClientProtobufServicerRequest/Response.proto defs.proto defs

HTTP/2 Connection — Binary Protobuf Messages

Protocol Buffers (Protobuf)

Protocol Buffers are Google's language-neutral, platform-neutral mechanism for serializing structured data. They are smaller, faster, and simpler than XML or JSON, using a compact binary format that makes them ideal for high-performance APIs.

Protobuf Syntax Basics

Protobuf definitions use .proto files to define messages (data structures) and services (RPC methods). The compiler generates code for your chosen language.

Example: service.proto

syntax = "proto3";

// Message definitions (like data models)
message User {
    string id = 1;          // Field number 1
    string username = 2;    // Field number 2
    string email = 3;       // Field number 3
    int32 age = 4;          // Field number 4
}

message GetUserRequest {
    string user_id = 1;
}

message GetUserResponse {
    User user = 1;
}

// Service definition (like API endpoints)
service UserService {
    rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
Field Numbers

Each field in a message has a unique number. These numbers identify fields in the binary format and should not change once your message is in use.

  • 1-15: Use one byte to encode (use for frequently used fields)
  • 16-2047: Use two bytes to encode
  • Reserved: Numbers 19000-19999 are reserved

Protobuf Data Types

Protobuf TypePython TypeDescriptionExample
stringstrUTF-8 encoded text"hello"
int32int32-bit signed integer42
int64int64-bit signed integer9999999999
boolboolBoolean valuetrue
floatfloat32-bit floating point3.14
doublefloat64-bit floating point3.141592653589793
bytesbytesArbitrary byte sequenceb"\x00\xFF"
repeatedlistRepeated field (array)[1, 2, 3]

Repeated Fields and Nested Messages

Example: Complex message structures

syntax = "proto3";

message Address {
    string street = 1;
    string city = 2;
    string country = 3;
    string postal_code = 4;
}

message User {
    string id = 1;
    string name = 2;
    repeated string tags = 3;           // Array of strings
    Address address = 4;                // Nested message
    repeated Address old_addresses = 5; // Array of nested messages
}

message UserList {
    repeated User users = 1;  // Array of User messages
    int32 total_count = 2;
}

Compiling Protobuf Files

Use the Protocol Buffer compiler (protoc) to generate Python code from your.proto files.

Install gRPC tools:

pip install grpcio grpcio-tools googleapis-common-protos mypy-protobuf

Compile .proto files (with type stubs):

python -m grpc_tools.protoc \
    -I definitions/ \
    --python_out=definitions/builds/ \
    --grpc_python_out=definitions/builds/ \
    --mypy_out=definitions/builds/ \
    definitions/service.proto

This generates three files:

  • service_pb2.py - Message classes (data structures)
  • service_pb2_grpc.py - Service classes (RPC stubs and servicers)
  • service_pb2.pyi - Type stubs for proper IDE support and type checking

REST vs gRPC vs GraphQL

Understanding the trade-offs between REST, gRPC, and GraphQL helps you choose the right tool for your use case. Each approach has distinct strengths and ideal scenarios.

Conceptual Model Differences

AspectRESTgRPCGraphQL
ModelResource-based (nouns)Action-based (verbs)Query-based (graph)
ProtocolHTTP/1.1 (usually)HTTP/2HTTP/1.1 or HTTP/2
PayloadJSON, XMLProtobuf (binary)JSON
ContractOpenAPI (optional).proto files (required)GraphQL Schema (required)
Type SafetyOptional (with tools)Strong (compile-time)Strong (runtime)
StreamingLimited (SSE, WebSocket)Native bidirectionalSubscriptions
Browser SupportNativeRequires gRPC-WebNative
Human ReadableYes (JSON)No (binary)Yes (JSON)
PerformanceModerateHighModerate-High
CachingHTTP cachingManualManual (complex)

Payload Size Comparison

One of gRPC's main advantages is the compact binary format of Protocol Buffers compared to JSON.

REST (JSON)
{
    "id": "user_123",
    "username": "alice"
}

Size: ~100 bytes

gRPC (Protobuf)
Binary format:
[field tags + values]
Not human-readable

Size: ~30-40 bytes
60-70% smaller!

GraphQL (JSON)
{ "data": {
    "user": {"username": "x"}
  }
}

Size: ~80-120 bytes (depends on query)

Use Case Matrix

Use CaseBest ChoiceWhy?
Public API for third-party developersRESTUniversal support, easy to use, well-documented
Microservices communicationgRPCHigh performance, type safety, streaming support
Mobile app backendGraphQL or gRPCReduce over-fetching (GraphQL) or bandwidth (gRPC)
Real-time data (chat, live updates)gRPC or GraphQL SubscriptionsNative streaming support
Simple CRUD operationsRESTResource model fits naturally, caching works well
Complex, nested data queriesGraphQLFlexible queries, reduce round trips
Browser-based applicationREST or GraphQLNative browser support (gRPC requires gRPC-Web)
Low-bandwidth, IoT devicesgRPCCompact binary format, efficient serialization
Service mesh / cloud-nativegRPCBuilt-in support in Kubernetes, Istio, Envoy

Building Your First gRPC Service

Let's build a complete gRPC service for managing tickets. We'll define Protocol Buffers messages, implement the server, and create a client to test it.

Setup: Create Project and Virtual Environment

First, create the project folder and set up a virtual environment:

mkdir grpc-ticket-service
cd grpc-ticket-service
virtualenv --python=python3.14 .venv
source .venv/bin/activate
pip install --upgrade pip

Project Structure

We'll create a ticket management service with the following structure:

grpc-ticket-service/
├── definitions/
│   ├── __init__.py
│   ├── service.proto           # Protobuf definitions
│   └── builds/
│       ├── __init__.py
│       ├── service_pb2.py      # Generated message classes
│       ├── service_pb2.pyi     # Generated type stubs
│       └── service_pb2_grpc.py # Generated service classes
├── server.py                   # gRPC server implementation
├── client.py                   # gRPC client example
└── requirements.txt

Step 1: Define Protobuf Schema

Create definitions/service.proto with message and service definitions:

File: definitions/service.proto

syntax = "proto3";

// Empty message for health check
message Null {}

// Message representing a ticket
message Ticket {
    string name = 1;
    string description = 2;
    uint32 story_points = 3;
}

// Response message with expected deadline
message Confirmation {
    string expected_dateline = 1;
}

// Service definition
service TicketService {
    // Health check endpoint
    rpc Health(Null) returns (Null);

    // Add a new ticket and get estimated completion date
    rpc AddTicket(Ticket) returns (Confirmation);
}

Step 2: Compile Protobuf Files

Install dependencies:

pip install grpcio grpcio-tools googleapis-common-protos mypy-protobuf

Compile the .proto file (with type stubs):

python -m grpc_tools.protoc \
    -I definitions/ \
    --python_out=definitions/builds/ \
    --grpc_python_out=definitions/builds/ \
    --mypy_out=definitions/builds/ \
    definitions/service.proto

This generates service_pb2.py (message classes),service_pb2_grpc.py (service stubs and servicers), andservice_pb2.pyi (type stubs for IDE support).

Step 3: Implement gRPC Server

Create the server by implementing the service methods defined in the .proto file:

File: server.py

# server.py
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone

import grpc

from definitions.builds.service_pb2 import Confirmation
from definitions.builds.service_pb2_grpc import (
    TicketServiceServicer,
    add_TicketServiceServicer_to_server
)


class TicketService(TicketServiceServicer):
    """Implementation of TicketService defined in .proto file"""

    def Health(self, request, context):
        """Health check endpoint - returns empty message"""
        print("Health check called")
        return request

    def AddTicket(self, request, context):
        """
        Add a ticket and calculate expected deadline
        based on story points (1 point = 1 day)
        """
        print(f"Adding ticket: {request.name}")
        print(f"  Description: {request.description}")
        print(f"  Story Points: {request.story_points}")

        # Calculate deadline: current time + story_points days
        expected_dateline = datetime.now(timezone.utc) + timedelta(days=request.story_points)

        return Confirmation(
            expected_dateline=expected_dateline.strftime("%Y-%m-%d %H:%M:%S")
        )


def serve():
    """Start the gRPC server"""
    # Create server with thread pool
    server = grpc.server(ThreadPoolExecutor(max_workers=10))

    # Register our service implementation
    add_TicketServiceServicer_to_server(TicketService(), server)

    # Listen on port 50051
    server.add_insecure_port("[::]:50051")

    # Start the server
    server.start()
    print("The server is up and running...")

    # Keep server running
    server.wait_for_termination()


if __name__ == "__main__":
    serve()

Start the server:

python server.py

Output:

The server is up and running...

Step 4: Implement gRPC Client

Create a client to call the server methods:

File: client.py

# client.py
import grpc

from definitions.builds.service_pb2 import Null, Ticket
from definitions.builds.service_pb2_grpc import TicketServiceStub


def main():
    # Create a channel to the server
    with grpc.insecure_channel("localhost:50051") as channel:
        # Create a stub (client)
        client = TicketServiceStub(channel)

        # Call Health check
        print("Calling Health()...")
        client.Health(Null())
        print("✓ Health check passed\n")

        # Call AddTicket
        print("Adding a ticket...")
        confirmation = client.AddTicket(Ticket(
            name="Implement user authentication",
            description="Add JWT-based authentication to the API",
            story_points=3
        ))

        print(f"✓ Ticket added successfully!")
        print(f"  Expected deadline: {confirmation.expected_dateline}")


if __name__ == "__main__":
    main()

Run the client (with server running):

python client.py

Client output:

Calling Health()...
✓ Health check passed

Adding a ticket...
✓ Ticket added successfully!
Expected deadline: 2026-12-31 00:00:00

Server output:

The server is up and running...
Health check called
Adding ticket: Implement user authentication
  Description: Add JWT-based authentication to the API
  Story Points: 3
What Just Happened?
  1. Client created a channel (connection) to the server
  2. Client created a stub (auto-generated client class)
  3. Client called AddTicket() with a Ticket message
  4. Message was serialized to Protobuf binary format
  5. Request sent over HTTP/2 to server
  6. Server deserialized the message, executed logic
  7. Server serialized the Confirmation response
  8. Client received and deserialized the response

Streaming Patterns in gRPC

gRPC supports four types of service methods: Unary (single request/response), Server Streaming (single request, stream of responses), Client Streaming (stream of requests, single response), and Bidirectional Streaming (stream of requests and responses).

gRPC Streaming Types

1. Unary RPC

Single request, single response (like REST).

rpc GetUser(UserRequest)
returns (UserResponse);
2. Server Streaming

Single request, stream of responses.

rpc ListUsers(Null)
returns (stream User);
3. Client Streaming

Stream of requests, single response.

rpc UploadLogs(stream LogEntry)
returns (UploadSummary);
4. Bidirectional Streaming

Stream of requests, stream of responses.

rpc Chat(stream Message)
returns (stream Message);

Server Streaming Example

Server streaming is useful when you need to return a large dataset or real-time updates.

Protobuf definition:

syntax = "proto3";

message TaskRequest {
    string project_id = 1;
}

message Task {
    string id = 1;
    string title = 2;
    string status = 3;
}

service TaskService {
    // Server streams all tasks for a project
    rpc ListTasks(TaskRequest) returns (stream Task);
}

Server implementation:

import time
from definitions.builds.service_pb2 import Task
from definitions.builds.service_pb2_grpc import TaskServiceServicer

class TaskService(TaskServiceServicer):
    def ListTasks(self, request, context):
        """Stream tasks one by one"""
        tasks = [
            {"id": "1", "title": "Design API", "status": "done"},
            {"id": "2", "title": "Implement auth", "status": "in_progress"},
            {"id": "3", "title": "Write tests", "status": "todo"},
        ]

        for task_data in tasks:
            # Simulate processing delay
            time.sleep(0.5)

            # Yield each task (streaming)
            yield Task(
                id=task_data["id"],
                title=task_data["title"],
                status=task_data["status"]
            )

Client implementation:

from definitions.builds.service_pb2 import TaskRequest
from definitions.builds.service_pb2_grpc import TaskServiceStub

with grpc.insecure_channel("localhost:50051") as channel:
    client = TaskServiceStub(channel)

    # Call streaming RPC
    task_stream = client.ListTasks(TaskRequest(project_id="proj_123"))

    # Iterate over the stream
    print("Receiving tasks...")
    for task in task_stream:
        print(f"  [{task.status}] {task.title} (ID: {task.id})")

Output:

Receiving tasks...
[done] Design API (ID: 1)
[in_progress] Implement auth (ID: 2)
[todo] Write tests (ID: 3)

Bidirectional Streaming Example

Bidirectional streaming enables real-time, two-way communication like chat applications.

Protobuf definition:

syntax = "proto3";

message ChatMessage {
    string username = 1;
    string text = 2;
    int64 timestamp = 3;
}

service ChatService {
    // Bidirectional streaming chat
    rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

Server implementation:

from definitions.builds.service_pb2_grpc import ChatServiceServicer
import time

class ChatService(ChatServiceServicer):
    def __init__(self):
        self.clients = []

    def Chat(self, request_iterator, context):
        """Bidirectional streaming chat"""
        # Add this client to the list
        self.clients.append(context)

        try:
            # Read messages from client
            for message in request_iterator:
                print(f"{message.username}: {message.text}")

                # Broadcast to all connected clients
                for client_context in self.clients:
                    if client_context != context:
                        yield message

        finally:
            # Remove client on disconnect
            self.clients.remove(context)

Client implementation:

import threading
import time
from definitions.builds.service_pb2 import ChatMessage
from definitions.builds.service_pb2_grpc import ChatServiceStub

def send_messages(stub):
    """Send messages to server"""
    messages = [
        ChatMessage(username="alice", text="Hello!", timestamp=int(time.time())),
        ChatMessage(username="alice", text="How are you?", timestamp=int(time.time())),
    ]

    for msg in messages:
        yield msg
        time.sleep(1)

def run_chat():
    with grpc.insecure_channel("localhost:50051") as channel:
        client = ChatServiceStub(channel)

        # Start bidirectional stream
        responses = client.Chat(send_messages(client))

        # Listen for responses
        for response in responses:
            print(f"Received: {response.username}: {response.text}")

run_chat()
When to Use Each Streaming Type
  • Unary: Simple request-response (user lookup, create resource)
  • Server Streaming: Large datasets, real-time updates (logs, metrics, notifications)
  • Client Streaming: Upload large files, batch inserts, telemetry data
  • Bidirectional: Chat, collaborative editing, real-time gaming

Performance Comparison: gRPC vs REST

Let's compare gRPC, FastAPI, and Flask performance with a real benchmark. We'll send 2,000 requests to equivalent endpoints and measure throughput.

Benchmark Setup

We'll create equivalent endpoints in gRPC, FastAPI, and Flask that calculate a ticket deadline based on story points, then measure throughput.

gRPC Implementation

gRPC test client (2,000 requests):

# grpc_benchmark.py
from time import time
import grpc

from definitions.builds.service_pb2 import Null, Ticket
from definitions.builds.service_pb2_grpc import TicketServiceStub


def main():
    with grpc.insecure_channel("localhost:50051") as channel:
        client = TicketServiceStub(channel)

        # Warm up
        client.Health(Null())

        # Benchmark
        start = time()

        for _ in range(2000):
            client.AddTicket(Ticket(
                name="SomeTicket",
                description="...",
                story_points=2
            ))

        elapsed = time() - start
        print(f"gRPC: {elapsed:.4f} seconds")
        print(f"Throughput: {2000/elapsed:.2f} req/s")


if __name__ == "__main__":
    main()

FastAPI Implementation

FastAPI server:

# fastapi_server.py
import json
from datetime import datetime, timedelta, timezone

import uvicorn
from fastapi import FastAPI
from starlette.requests import Request

app = FastAPI()


@app.get("/health")
def health():
    return {"status": "ok"}


@app.post("/ticket")
async def manage_tickets(request: Request):
    body = json.loads(await request.body())
    points = body.get("story_points")
    expected_dateline = datetime.now(timezone.utc) + timedelta(days=points)
    return {"expected_dateline": expected_dateline.strftime("%Y-%m-%d %H:%M:%S")}


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=3001)

Start server:

python fastapi_server.py

Flask Implementation

Flask server:

# flask_server.py
import json
from datetime import datetime, timedelta, timezone

from flask import Flask, request

app = Flask(__name__)


@app.route("/")
def health():
    return "OK"


@app.route("/ticket", methods=["POST"])
def manage_tickets():
    points = json.loads(request.data).get("story_points")
    expected_dateline = datetime.now(timezone.utc) + timedelta(days=points)
    return {"expected_dateline": expected_dateline.strftime("%Y-%m-%d %H:%M:%S")}


if __name__ == "__main__":
    app.run(port=3001)

Start server:

python flask_server.py

REST Test Client

HTTP test client (for FastAPI and Flask):

# rest_benchmark.py
import json
from time import time

import urllib3

http = urllib3.PoolManager()


def main():
    start = time()

    for _ in range(2000):
        http.request(
            "POST",
            "http://localhost:3001/ticket",
            headers={"Content-Type": "application/json"},
            body=json.dumps({
                "name": "x",
                "description": "...",
                "story_points": 3
            })
        )

    elapsed = time() - start
    print(f"REST: {elapsed:.4f} seconds")
    print(f"Throughput: {2000/elapsed:.2f} req/s")


if __name__ == "__main__":
    main()

Benchmark Results

Tested with Python 3.14 on ASUS ROG Zephyrus M16 (Intel i7-12700H, 40GB RAM, Ubuntu 26.04 LTS)

FrameworkRun 1 (seconds)Run 2 (seconds)Run 3 (seconds)AverageThroughput (req/s)
gRPC0.28310.28750.29400.28826,941 req/s
FastAPI0.68360.68130.68280.68262,930 req/s
Flask1.98891.99021.99161.99021,005 req/s
C++ (Drogon)0.24240.24480.24530.24428,191 req/s

2.4x

gRPC faster than FastAPI

6.9x

gRPC faster than Flask

2.9x

FastAPI faster than Flask

Need Even More Performance? Consider C++

If your workload demands maximum throughput, C++ with the Drogon framework outperforms even gRPC/Python at 8,191 req/s - roughly 1.2x faster than gRPC and 8.2x faster than Flask.

A working example is available at gitlab.com/alek.cora.glez/c-plus-plus/api-service-c-plus-plus

Why is gRPC Faster?
  • Binary Serialization: Protobuf is 3-10x smaller than JSON
  • HTTP/2: Multiplexing, header compression, persistent connections
  • Less Parsing: Binary format requires less CPU than JSON parsing
  • Code Generation: Optimized serialization/deserialization code
  • Connection Reuse: HTTP/2 keeps connections open longer

When to Use gRPC vs REST

Understanding when to choose gRPC over REST (or vice versa) is crucial for building efficient, maintainable systems. Each has ideal use cases based on your requirements.

Use gRPC When:

Microservices Communication

Perfect for: Internal service-to-service communication in a microservice architecture. Type safety, performance, and streaming support make gRPC ideal for internal APIs.

Real-Time Communication

Perfect for: Chat applications, live updates, collaborative editing, real-time dashboards. Bidirectional streaming enables efficient real-time communication.

High-Performance Requirements

Perfect for: Systems where latency and bandwidth matter (financial trading, gaming, IoT). Protobuf's compact format and HTTP/2 efficiency provide significant performance gains.

Type Safety & Contracts

Perfect for: When you need strong API contracts between teams. Protobuf definitions enforce type safety and auto-generate client/server code, reducing integration errors.

Use REST When:

Public APIs

Best for: Third-party APIs consumed by external developers. REST's widespread adoption, browser support, and human-readable JSON make it the standard for public APIs.

Browser-Based Applications

Best for: Web applications with browser clients. Native browser support for REST (fetch, XMLHttpRequest) makes it simpler than gRPC-Web.

Resource-Oriented APIs

Best for: CRUD operations on resources (users, products, orders). REST's resource model maps naturally to database entities and HTTP caching works well.

Ecosystem & Tooling

Best for: When you need mature tooling (API gateways, monitoring, debugging tools). REST has extensive ecosystem support and easier debugging with human-readable payloads.

Hybrid Approach

Many Organizations Use Both

It's common to use gRPC for internal services and REST for public APIs:

  • External: REST API for mobile apps, third-party integrations
  • Internal: gRPC between microservices for performance
  • Gateway: API Gateway translates REST → gRPC for external clients

REST / gRPC Gateway Pattern

Mobile AppWeb BrowserAPI GatewayREST → gRPCAuth ServicegRPCUser ServicegRPCOrder ServicegRPCREST / JSONREST / JSONgRPCgRPCgRPC

Production Best Practices

Essential practices for running gRPC services in production. Security, reliability, and observability are critical for production-grade gRPC deployments.

Security: TLS Encryption

Always use TLS in production. Never use insecure_channel for real services.

Server with TLS:

import grpc
from concurrent.futures import ThreadPoolExecutor

def serve_with_tls():
    # Read TLS credentials
    with open("server.key", "rb") as f:
        private_key = f.read()
    with open("server.crt", "rb") as f:
        certificate_chain = f.read()

    # Create server credentials
    server_credentials = grpc.ssl_server_credentials(
        [(private_key, certificate_chain)]
    )

    # Create server
    server = grpc.server(ThreadPoolExecutor(max_workers=10))
    add_TicketServiceServicer_to_server(TicketService(), server)

    # Add secure port
    server.add_secure_port("[::]:50051", server_credentials)

    server.start()
    print("Secure gRPC server started on port 50051")
    server.wait_for_termination()

Client with TLS:

import grpc

# Read root certificates
with open("ca.crt", "rb") as f:
    trusted_certs = f.read()

# Create credentials
credentials = grpc.ssl_channel_credentials(root_certificates=trusted_certs)

# Create secure channel
with grpc.secure_channel("api.example.com:50051", credentials) as channel:
    client = TicketServiceStub(channel)
    response = client.AddTicket(ticket)

Error Handling and Status Codes

Use gRPC status codes to communicate errors clearly to clients.

gRPC StatusHTTP EquivalentUse Case
OK200Success
INVALID_ARGUMENT400Client provided invalid data
UNAUTHENTICATED401Missing or invalid authentication
PERMISSION_DENIED403Authenticated but not authorized
NOT_FOUND404Resource doesn't exist
ALREADY_EXISTS409Resource already exists
INTERNAL500Server error
UNAVAILABLE503Service temporarily unavailable

Server error handling:

import grpc
from definitions.builds.service_pb2_grpc import TicketServiceServicer

class TicketService(TicketServiceServicer):
    def AddTicket(self, request, context):
        # Validate input
        if not request.name:
            context.abort(
                grpc.StatusCode.INVALID_ARGUMENT,
                "Ticket name is required"
            )

        if request.story_points < 1 or request.story_points > 13:
            context.abort(
                grpc.StatusCode.INVALID_ARGUMENT,
                "Story points must be between 1 and 13"
            )

        try:
            # Business logic
            deadline = calculate_deadline(request.story_points)
            return Confirmation(expected_dateline=deadline)

        except Exception as e:
            # Log the error
            print(f"Error: {e}")

            # Return generic error to client
            context.abort(
                grpc.StatusCode.INTERNAL,
                "An internal error occurred"
            )

Client error handling:

import grpc

try:
    response = client.AddTicket(ticket)
    print(f"Success: {response.expected_dateline}")

except grpc.RpcError as e:
    # Check status code
    if e.code() == grpc.StatusCode.INVALID_ARGUMENT:
        print(f"Invalid input: {e.details()}")
    elif e.code() == grpc.StatusCode.NOT_FOUND:
        print(f"Not found: {e.details()}")
    elif e.code() == grpc.StatusCode.UNAVAILABLE:
        print("Service unavailable, retrying...")
        # Implement retry logic
    else:
        print(f"Error: {e.code()} - {e.details()}")

Health Checks

Implement health checks for load balancers and orchestration systems (Kubernetes).

Install health checking:

pip install grpcio-health-checking

Server with health checking:

import grpc
from grpc_health.v1 import health, health_pb2, health_pb2_grpc
from concurrent.futures import ThreadPoolExecutor

def serve_with_health_check():
    server = grpc.server(ThreadPoolExecutor(max_workers=10))

    # Add your service
    add_TicketServiceServicer_to_server(TicketService(), server)

    # Add health check service
    health_servicer = health.HealthServicer()
    health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)

    # Set service status to SERVING
    health_servicer.set("TicketService", health_pb2.HealthCheckResponse.SERVING)

    server.add_insecure_port("[::]:50051")
    server.start()
    server.wait_for_termination()

Timeouts and Deadlines

Always set deadlines to prevent requests from hanging indefinitely.

Client with timeout:

import grpc

with grpc.insecure_channel("localhost:50051") as channel:
client = TicketServiceStub(channel)

try:
    # Set 5 second deadline
    response = client.AddTicket(
        ticket,
        timeout=5.0  # 5 seconds
    )
    print(f"Response: {response.expected_dateline}")

except grpc.RpcError as e:
    if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
        print("Request timed out after 5 seconds")

Observability: Logging and Metrics

Instrument your gRPC services with logging, metrics, and tracing.

Install Prometheus instrumentation:

pip install prometheus-client py-grpc-prometheus

Server with Prometheus metrics:

import grpc
from py_grpc_prometheus.prometheus_server_interceptor import PromServerInterceptor
from prometheus_client import start_http_server

def serve_with_metrics():
# Start Prometheus metrics server on port 8000
start_http_server(8000)

# Create interceptor
interceptor = PromServerInterceptor()

# Create server with interceptor
server = grpc.server(
    ThreadPoolExecutor(max_workers=10),
    interceptors=[interceptor]
)

add_TicketServiceServicer_to_server(TicketService(), server)
server.add_insecure_port("[::]:50051")

server.start()
print("gRPC server on :50051, metrics on :8000")
server.wait_for_termination()

Metrics available at http://localhost:8000/metrics:

  • grpc_server_started_total - Total RPCs started
  • grpc_server_handled_total - Total RPCs completed
  • grpc_server_handling_seconds - RPC latency distribution
  • grpc_server_msg_received_total - Messages received
  • grpc_server_msg_sent_total - Messages sent
Production Checklist
  • ✓ Use TLS encryption for all production traffic
  • ✓ Implement proper error handling with status codes
  • ✓ Add health check endpoints for orchestration
  • ✓ Set timeouts/deadlines on all client requests
  • ✓ Enable metrics and monitoring (Prometheus, DataDog, etc.)
  • ✓ Implement structured logging with correlation IDs
  • ✓ Use connection pooling and keep-alive settings
  • ✓ Test with load testing tools (ghz, k6)
  • ✓ Document your .proto files with comments
  • ✓ Version your protobuf definitions carefully
Final Thoughts: Choosing the Right Tool

gRPC is a very good framework with a lot of benefits and maybe in the future will become dominant, but for sure REST will be around for a long time. Frequently we find ourselves looking for comparisons, but the key is NOT one tool versus another one.

The key is to find the best tool that helps us solve our use-case better. The requirements are the foundation, that's where the decision starts. Don't choose gRPC because it's "faster" or REST because it's "simpler." Choose based on your specific needs: team expertise, infrastructure constraints, client requirements, performance targets, and long-term maintenance considerations. The best tool is the one that solves your problems effectively, not the one that looks best on paper.

Key Takeaways

  • gRPC is a high-performance RPC framework using Protocol Buffers and HTTP/2
  • Protobuf provides type-safe, compact binary serialization (60-70% smaller than JSON)
  • Performance: gRPC is 3-5x faster than REST for many workloads
  • Streaming: Native support for server, client, and bidirectional streaming
  • Use gRPC for microservices, real-time systems, and performance-critical applications
  • Use REST for public APIs, browser-based apps, and resource-oriented services
  • Hybrid approach: Many organizations use both (REST externally, gRPC internally)
  • Production: Always use TLS, health checks, timeouts, and observability