Skip to content

gRPC & Protocol Buffers

What is RPC?

Remote Procedure Call (RPC) is a communication paradigm where a client invokes a function on a remote server as if it were a local function call. Unlike REST, which is resource-oriented (nouns and HTTP methods), RPC is action-oriented — you call specific procedures with defined inputs and outputs.

// REST: resource-oriented
GET /users/42/orders?status=pending
// RPC: action-oriented
GetPendingOrders(user_id=42)

While the concept of RPC dates back to the 1980s, modern frameworks like gRPC have made it practical, performant, and polyglot.


Why gRPC?

gRPC (gRPC Remote Procedure Calls) is a high-performance, open-source RPC framework originally developed by Google. It is now a Cloud Native Computing Foundation (CNCF) incubating project used at scale by companies like Google, Netflix, Dropbox, and Square.

Key Advantages

  • HTTP/2 Transport — Multiplexed streams, header compression, and bidirectional communication over a single TCP connection
  • Protocol Buffers — Efficient binary serialization that is 3-10x smaller and 20-100x faster than JSON
  • Code Generation — Generate client and server code in 12+ languages from a single .proto file
  • Streaming — Native support for server streaming, client streaming, and bidirectional streaming
  • Deadlines and Cancellation — Built-in support for timeouts and request cancellation
  • Interceptors — Middleware for authentication, logging, and monitoring

When to Use gRPC

  • Microservice-to-microservice communication where performance matters
  • Polyglot environments where services are written in different languages
  • Real-time streaming scenarios (live data feeds, chat, telemetry)
  • Low-latency, high-throughput internal APIs
  • Mobile clients on constrained networks (smaller payloads)

When NOT to Use gRPC

  • Browser clients (gRPC-Web exists but adds complexity)
  • Public APIs where developer experience and simplicity matter
  • Simple CRUD applications where REST is sufficient
  • Teams unfamiliar with Protocol Buffers and code generation workflows

Protocol Buffers

Protocol Buffers (protobuf) is Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data. It serves as both the Interface Definition Language (IDL) and the serialization format for gRPC.

Message Definition

Messages are the primary data structures in protobuf.

blog.proto
syntax = "proto3";
package blog.v1;
option java_package = "com.example.blog.v1";
option go_package = "github.com/example/blog/v1";
message Post {
string id = 1;
string title = 2;
string content = 3;
PostStatus status = 4;
string author_id = 5;
repeated string tags = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
message User {
string id = 1;
string name = 2;
string email = 3;
string avatar_url = 4;
}
message Comment {
string id = 1;
string body = 2;
string author_id = 3;
string post_id = 4;
google.protobuf.Timestamp created_at = 5;
}

Field Numbers

Each field has a unique number that identifies it in the binary encoding. These numbers must never change once your message is in use.

  • Fields 1-15 use 1 byte for the tag (use for frequently-set fields)
  • Fields 16-2047 use 2 bytes
  • Never reuse a field number after deleting a field (use reserved instead)
message Post {
reserved 6, 9; // Reserved field numbers
reserved "category", "slug"; // Reserved field names
string id = 1;
string title = 2;
// Field 6 was "category", now removed
}

Scalar Types

Protobuf TypePython TypeJava TypeGo TypeDescription
doublefloatdoublefloat6464-bit floating point
floatfloatfloatfloat3232-bit floating point
int32intintint32Variable-length signed
int64intlongint64Variable-length signed
uint32intintuint32Variable-length unsigned
uint64intlonguint64Variable-length unsigned
boolboolbooleanboolBoolean
stringstrStringstringUTF-8 or ASCII
bytesbytesByteString[]byteArbitrary byte data

Enum Types

enum PostStatus {
POST_STATUS_UNSPECIFIED = 0; // Always have a zero value
POST_STATUS_DRAFT = 1;
POST_STATUS_PUBLISHED = 2;
POST_STATUS_ARCHIVED = 3;
}

The zero value should always be an UNSPECIFIED or UNKNOWN sentinel value so that default (unset) fields are distinguishable from intentionally set fields.

Nested Messages

message PostResponse {
Post post = 1;
AuthorInfo author = 2;
message AuthorInfo {
string name = 1;
string avatar_url = 2;
int32 post_count = 3;
}
}

Oneof

Use oneof when only one of several fields should be set at a time.

message NotificationTarget {
string message = 1;
oneof target {
string email = 2;
string phone_number = 3;
string push_token = 4;
}
}

Maps

message PostMetadata {
string id = 1;
map<string, string> labels = 2; // e.g., {"language": "en", "region": "us"}
map<string, int32> view_counts = 3; // e.g., {"2025-06-01": 142, "2025-06-02": 87}
}

Service Definitions

gRPC services are defined in .proto files alongside message types.

blog_service.proto
syntax = "proto3";
package blog.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
service BlogService {
// Unary RPC
rpc GetPost(GetPostRequest) returns (GetPostResponse);
rpc CreatePost(CreatePostRequest) returns (CreatePostResponse);
rpc UpdatePost(UpdatePostRequest) returns (UpdatePostResponse);
rpc DeletePost(DeletePostRequest) returns (google.protobuf.Empty);
// Server streaming RPC
rpc ListPosts(ListPostsRequest) returns (stream Post);
// Client streaming RPC
rpc BatchCreatePosts(stream CreatePostRequest) returns (BatchCreatePostsResponse);
// Bidirectional streaming RPC
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetPostRequest {
string id = 1;
}
message GetPostResponse {
Post post = 1;
}
message CreatePostRequest {
string title = 1;
string content = 2;
repeated string tags = 3;
}
message CreatePostResponse {
Post post = 1;
}
message UpdatePostRequest {
string id = 1;
string title = 2;
string content = 3;
repeated string tags = 4;
PostStatus status = 5;
}
message UpdatePostResponse {
Post post = 1;
}
message DeletePostRequest {
string id = 1;
}
message ListPostsRequest {
int32 page_size = 1;
string page_token = 2;
PostStatus status_filter = 3;
}
message BatchCreatePostsResponse {
int32 created_count = 1;
repeated string post_ids = 2;
}
message ChatMessage {
string user_id = 1;
string content = 2;
google.protobuf.Timestamp timestamp = 3;
}

Four Types of RPC

gRPC supports four communication patterns.

1. Unary RPC

The simplest pattern: client sends one request, server sends one response. Similar to a traditional function call.

rpc GetPost(GetPostRequest) returns (GetPostResponse);
Client ──request──> Server
Client <──response── Server

2. Server Streaming RPC

Client sends one request, server sends a stream of responses. Useful for large datasets, real-time feeds, or long-running queries.

rpc ListPosts(ListPostsRequest) returns (stream Post);
Client ──request──> Server
Client <──response── Server
Client <──response── Server
Client <──response── Server
Client <──(done)──── Server

3. Client Streaming RPC

Client sends a stream of requests, server sends one response after the client is done. Useful for batch uploads or aggregation.

rpc BatchCreatePosts(stream CreatePostRequest) returns (BatchCreatePostsResponse);
Client ──request──> Server
Client ──request──> Server
Client ──request──> Server
Client ──(done)───> Server
Client <──response── Server

4. Bidirectional Streaming RPC

Both client and server send streams of messages independently. The two streams operate independently, so client and server can read and write in any order.

rpc Chat(stream ChatMessage) returns (stream ChatMessage);
Client ──message──> Server
Client <──message── Server
Client ──message──> Server
Client ──message──> Server
Client <──message── Server
Client <──message── Server

Code Generation Workflow

The core workflow for gRPC involves defining .proto files and generating code.

┌─────────────┐
│ .proto │
│ files │
└──────┬──────┘
protoc compiler
+ language plugins
┌────────────────┼────────────────┐
│ │ │
┌─────▼──────┐ ┌─────▼──────┐ ┌─────▼──────┐
│ Python │ │ Java │ │ Go │
│ stubs │ │ stubs │ │ stubs │
└────────────┘ └────────────┘ └────────────┘

Generating Code

Terminal window
# Install the tools
pip install grpcio grpcio-tools
# Generate Python code from proto files
python -m grpc_tools.protoc \
--proto_path=./proto \
--python_out=./generated \
--grpc_python_out=./generated \
./proto/blog_service.proto
# This generates two files:
# blog_service_pb2.py - Message classes
# blog_service_pb2_grpc.py - Client stub and server base classes

Implementing the Server

server.py
import grpc
from concurrent import futures
from generated import blog_service_pb2
from generated import blog_service_pb2_grpc
class BlogServicer(blog_service_pb2_grpc.BlogServiceServicer):
"""Implementation of the BlogService."""
def __init__(self):
self.posts = {} # In-memory store for demo
def GetPost(self, request, context):
"""Unary RPC: Get a single post by ID."""
post = self.posts.get(request.id)
if post is None:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details(f"Post {request.id} not found")
return blog_service_pb2.GetPostResponse()
return blog_service_pb2.GetPostResponse(post=post)
def CreatePost(self, request, context):
"""Unary RPC: Create a new post."""
import uuid
post_id = str(uuid.uuid4())
post = blog_service_pb2.Post(
id=post_id,
title=request.title,
content=request.content,
tags=request.tags,
status=blog_service_pb2.POST_STATUS_DRAFT,
)
self.posts[post_id] = post
return blog_service_pb2.CreatePostResponse(post=post)
def ListPosts(self, request, context):
"""Server streaming RPC: Stream all posts."""
for post in self.posts.values():
if (
request.status_filter
== blog_service_pb2.POST_STATUS_UNSPECIFIED
or post.status == request.status_filter
):
yield post
def BatchCreatePosts(self, request_iterator, context):
"""Client streaming RPC: Batch create posts."""
created_ids = []
for request in request_iterator:
import uuid
post_id = str(uuid.uuid4())
post = blog_service_pb2.Post(
id=post_id,
title=request.title,
content=request.content,
tags=request.tags,
status=blog_service_pb2.POST_STATUS_DRAFT,
)
self.posts[post_id] = post
created_ids.append(post_id)
return blog_service_pb2.BatchCreatePostsResponse(
created_count=len(created_ids),
post_ids=created_ids,
)
def Chat(self, request_iterator, context):
"""Bidirectional streaming RPC."""
for message in request_iterator:
# Echo back with a response
response = blog_service_pb2.ChatMessage(
user_id="bot",
content=f"Received: {message.content}",
)
yield response
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
blog_service_pb2_grpc.add_BlogServiceServicer_to_server(
BlogServicer(), server
)
server.add_insecure_port("[::]:50051")
server.start()
print("Server started on port 50051")
server.wait_for_termination()
if __name__ == "__main__":
serve()

Implementing the Client

client.py
import grpc
from generated import blog_service_pb2
from generated import blog_service_pb2_grpc
def run():
# Create a channel and stub
channel = grpc.insecure_channel("localhost:50051")
stub = blog_service_pb2_grpc.BlogServiceStub(channel)
# Unary RPC: Create a post
create_response = stub.CreatePost(
blog_service_pb2.CreatePostRequest(
title="Hello gRPC",
content="This is a post created via gRPC.",
tags=["grpc", "protobuf"],
)
)
print(f"Created post: {create_response.post.id}")
# Unary RPC: Get a post
get_response = stub.GetPost(
blog_service_pb2.GetPostRequest(id=create_response.post.id)
)
print(f"Got post: {get_response.post.title}")
# Server streaming RPC: List all posts
print("All posts:")
for post in stub.ListPosts(blog_service_pb2.ListPostsRequest()):
print(f" - {post.title} ({post.id})")
# Handle errors
try:
stub.GetPost(
blog_service_pb2.GetPostRequest(id="nonexistent")
)
except grpc.RpcError as e:
print(f"Error: {e.code()} - {e.details()}")
# Output: Error: StatusCode.NOT_FOUND - Post nonexistent not found
if __name__ == "__main__":
run()

Error Handling (Status Codes)

gRPC uses its own set of status codes, similar in concept to HTTP status codes but specific to RPC semantics.

gRPC CodeNumberDescriptionHTTP Equivalent
OK0Success200
CANCELLED1Operation was cancelled499
UNKNOWN2Unknown error500
INVALID_ARGUMENT3Client sent invalid argument400
DEADLINE_EXCEEDED4Deadline expired504
NOT_FOUND5Resource not found404
ALREADY_EXISTS6Resource already exists409
PERMISSION_DENIED7Not authorized403
RESOURCE_EXHAUSTED8Rate limit or quota exceeded429
FAILED_PRECONDITION9Operation precondition not met400
ABORTED10Operation was aborted409
OUT_OF_RANGE11Value out of valid range400
UNIMPLEMENTED12Method not implemented501
INTERNAL13Internal server error500
UNAVAILABLE14Service temporarily unavailable503
DATA_LOSS15Unrecoverable data loss or corruption500
UNAUTHENTICATED16Not authenticated401

Rich Error Details

gRPC supports attaching structured error details using the google.rpc.Status message.

from grpc_status import rpc_status
from google.protobuf import any_pb2
from google.rpc import status_pb2, error_details_pb2
# Server: return rich error details
def CreatePost(self, request, context):
if not request.title:
detail = any_pb2.Any()
detail.Pack(
error_details_pb2.BadRequest(
field_violations=[
error_details_pb2.BadRequest.FieldViolation(
field="title",
description="Title is required"
)
]
)
)
rich_status = status_pb2.Status(
code=grpc.StatusCode.INVALID_ARGUMENT.value[0],
message="Validation failed",
details=[detail],
)
context.abort_with_status(rpc_status.to_status(rich_status))

Deadlines and Cancellation

Deadlines

Every gRPC call should have a deadline — the maximum time a client is willing to wait. If the deadline expires, the call is terminated with DEADLINE_EXCEEDED.

# Python client: set a 5-second deadline
try:
response = stub.GetPost(
blog_service_pb2.GetPostRequest(id="42"),
timeout=5.0, # 5 seconds
)
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
print("Request timed out!")
// Java client: set a 5-second deadline
try {
GetPostResponse response = stub
.withDeadlineAfter(5, TimeUnit.SECONDS)
.getPost(GetPostRequest.newBuilder().setId("42").build());
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.DEADLINE_EXCEEDED) {
System.out.println("Request timed out!");
}
}

Cancellation

Clients can cancel in-progress requests. Servers should check for cancellation in long-running operations.

# Server: check for cancellation in streaming RPCs
def ListPosts(self, request, context):
for post in self.get_all_posts():
if context.is_active(): # Check if client is still listening
yield post
else:
return # Client cancelled, stop processing

Interceptors (Middleware)

Interceptors are the gRPC equivalent of middleware. They allow you to add cross-cutting concerns like logging, authentication, and metrics.

# Python: Server interceptor for logging
import grpc
import time
class LoggingInterceptor(grpc.ServerInterceptor):
def intercept_service(self, continuation, handler_call_details):
method = handler_call_details.method
start_time = time.time()
print(f"[gRPC] Received call: {method}")
response = continuation(handler_call_details)
duration = time.time() - start_time
print(f"[gRPC] Completed {method} in {duration:.3f}s")
return response
# Apply interceptor to server
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
interceptors=[LoggingInterceptor()],
)
# Python: Client interceptor for authentication
class AuthInterceptor(grpc.UnaryUnaryClientInterceptor):
def __init__(self, token):
self.token = token
def intercept_unary_unary(self, continuation, client_call_details, request):
metadata = list(client_call_details.metadata or [])
metadata.append(("authorization", f"Bearer {self.token}"))
new_details = client_call_details._replace(metadata=metadata)
return continuation(new_details, request)
# Apply interceptor to channel
channel = grpc.intercept_channel(
grpc.insecure_channel("localhost:50051"),
AuthInterceptor(token="my-jwt-token"),
)

Performance Comparison

MetricREST + JSONgRPC + Protobuf
Payload size~100 bytes (JSON)~30 bytes (binary protobuf)
Serialization speed~1x (baseline)~20-100x faster
LatencyHigher (HTTP/1.1 overhead)Lower (HTTP/2, multiplexing)
StreamingSSE or WebSocket (separate)Native, bidirectional
Code generationOptional (OpenAPI)Required, strongly typed
Browser supportNativeRequires gRPC-Web proxy
Human readabilityJSON is readableBinary is not readable
DebuggingEasy (curl, browser)Needs specialized tools (grpcurl, Evans)

When Performance Matters

For internal microservice communication with hundreds of calls per second, gRPC’s binary serialization and HTTP/2 multiplexing provide measurable improvements. For public APIs with moderate traffic, the developer experience benefits of REST/JSON often outweigh the performance gains of gRPC.


Summary

gRPC and Protocol Buffers provide a powerful, type-safe, and performant foundation for building APIs, particularly in microservice architectures. Key takeaways:

  • Protocol Buffers define both the schema and serialization format in a single .proto file
  • Code generation produces strongly typed clients and servers in 12+ languages
  • Four RPC types (unary, server streaming, client streaming, bidirectional) cover all communication patterns
  • gRPC status codes provide semantic error handling similar to HTTP status codes
  • Deadlines and cancellation are first-class features for building resilient systems
  • Interceptors enable cross-cutting concerns like auth, logging, and metrics
  • gRPC excels for internal services but REST remains the better choice for public APIs

Next Steps