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:
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
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 Type | Python Type | Description | Example |
|---|---|---|---|
string | str | UTF-8 encoded text | "hello" |
int32 | int | 32-bit signed integer | 42 |
int64 | int | 64-bit signed integer | 9999999999 |
bool | bool | Boolean value | true |
float | float | 32-bit floating point | 3.14 |
double | float | 64-bit floating point | 3.141592653589793 |
bytes | bytes | Arbitrary byte sequence | b"\x00\xFF" |
repeated | list | Repeated 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.protoThis 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
| Aspect | REST | gRPC | GraphQL |
|---|---|---|---|
| Model | Resource-based (nouns) | Action-based (verbs) | Query-based (graph) |
| Protocol | HTTP/1.1 (usually) | HTTP/2 | HTTP/1.1 or HTTP/2 |
| Payload | JSON, XML | Protobuf (binary) | JSON |
| Contract | OpenAPI (optional) | .proto files (required) | GraphQL Schema (required) |
| Type Safety | Optional (with tools) | Strong (compile-time) | Strong (runtime) |
| Streaming | Limited (SSE, WebSocket) | Native bidirectional | Subscriptions |
| Browser Support | Native | Requires gRPC-Web | Native |
| Human Readable | Yes (JSON) | No (binary) | Yes (JSON) |
| Performance | Moderate | High | Moderate-High |
| Caching | HTTP caching | Manual | Manual (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 Case | Best Choice | Why? |
|---|---|---|
| Public API for third-party developers | REST | Universal support, easy to use, well-documented |
| Microservices communication | gRPC | High performance, type safety, streaming support |
| Mobile app backend | GraphQL or gRPC | Reduce over-fetching (GraphQL) or bandwidth (gRPC) |
| Real-time data (chat, live updates) | gRPC or GraphQL Subscriptions | Native streaming support |
| Simple CRUD operations | REST | Resource model fits naturally, caching works well |
| Complex, nested data queries | GraphQL | Flexible queries, reduce round trips |
| Browser-based application | REST or GraphQL | Native browser support (gRPC requires gRPC-Web) |
| Low-bandwidth, IoT devices | gRPC | Compact binary format, efficient serialization |
| Service mesh / cloud-native | gRPC | Built-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.protoThis 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?
- Client created a channel (connection) to the server
- Client created a stub (auto-generated client class)
- Client called
AddTicket()with a Ticket message - Message was serialized to Protobuf binary format
- Request sent over HTTP/2 to server
- Server deserialized the message, executed logic
- Server serialized the Confirmation response
- 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)
| Framework | Run 1 (seconds) | Run 2 (seconds) | Run 3 (seconds) | Average | Throughput (req/s) |
|---|---|---|---|---|---|
| gRPC | 0.2831 | 0.2875 | 0.2940 | 0.2882 | 6,941 req/s |
| FastAPI | 0.6836 | 0.6813 | 0.6828 | 0.6826 | 2,930 req/s |
| Flask | 1.9889 | 1.9902 | 1.9916 | 1.9902 | 1,005 req/s |
| C++ (Drogon) | 0.2424 | 0.2448 | 0.2453 | 0.2442 | 8,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
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 Status | HTTP Equivalent | Use Case |
|---|---|---|
OK | 200 | Success |
INVALID_ARGUMENT | 400 | Client provided invalid data |
UNAUTHENTICATED | 401 | Missing or invalid authentication |
PERMISSION_DENIED | 403 | Authenticated but not authorized |
NOT_FOUND | 404 | Resource doesn't exist |
ALREADY_EXISTS | 409 | Resource already exists |
INTERNAL | 500 | Server error |
UNAVAILABLE | 503 | Service 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 startedgrpc_server_handled_total- Total RPCs completedgrpc_server_handling_seconds- RPC latency distributiongrpc_server_msg_received_total- Messages receivedgrpc_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