Networking & Communication
High-performance RPC framework using binary serialization. When to choose gRPC over REST, and how to design service contracts with Protocol Buffers.
When a microservice calls another microservice millions of times per second, the overhead of REST+JSON adds up. Each request parses human-readable JSON (slow), sends verbose field names over the wire (wasteful), and uses HTTP/1.1's one-request-per-connection model (inefficient). gRPC, developed by Google, replaces this with binary serialization (Protocol Buffers), HTTP/2 multiplexing, and strongly typed service contracts. It is not a silver bullet -- REST is still better for many use cases -- but for internal service-to-service communication at scale, gRPC is often the right choice.
Protocol Buffers are Google's language-neutral, platform-neutral mechanism for serializing structured data. Think of it as a more efficient, strongly typed alternative to JSON.
// user.proto
syntax = "proto3";
package userservice;
message User {
int64 id = 1; // Field number 1
string name = 2; // Field number 2
string email = 3; // Field number 3
repeated string roles = 4; // List of strings
Address address = 5; // Nested message
enum Status {
UNKNOWN = 0;
ACTIVE = 1;
INACTIVE = 2;
}
Status status = 6;
}
message Address {
string street = 1;
string city = 2;
string country = 3;
string zip_code = 4;
}The key insight: protobuf encodes data using field numbers (not field names) and a compact binary format. Compare:
JSON (95 bytes):
{"id":42,"name":"Alice","email":"alice@example.com","roles":["admin","user"],"status":"ACTIVE"}
Protobuf (approximately 45 bytes):
[binary: field_1=42, field_2="Alice", field_3="alice@example.com",
field_4=["admin","user"], field_6=1]Protobuf is typically 3-10x smaller than JSON and 20-100x faster to serialize/deserialize. For a service handling millions of requests per second, this translates to significant savings in network bandwidth, CPU usage, and memory allocation.
Each field has a number (1, 2, 3...) that identifies it in the binary format. The field name is only used in your code; it is never transmitted over the wire.
Wire format for field number 1, value 42:
[08] [2A]
─┬── ─┬──
│ └── Value: 42 (varint encoding)
└── Field 1, wire type 0 (varint)
Wire format for field number 2, value "Alice":
[12] [05] [41 6C 69 63 65]
─┬── ─┬── ──────┬────────
│ │ └── UTF-8 bytes: "Alice"
│ └── Length: 5 bytes
└── Field 2, wire type 2 (length-delimited)This is why protobuf is so compact: field names like "email" or "address" are never sent. Only their numeric identifiers.
gRPC uses Protocol Buffers as both its interface definition language (IDL) and its serialization format. You define services and their methods in a .proto file, and gRPC generates client and server code in your chosen language.
// user_service.proto
syntax = "proto3";
package userservice;
service UserService {
// Unary RPC: one request, one response
rpc GetUser(GetUserRequest) returns (GetUserResponse);
// Server streaming: one request, stream of responses
rpc ListUsers(ListUsersRequest) returns (stream User);
// Client streaming: stream of requests, one response
rpc UploadUserPhotos(stream PhotoUpload) returns (UploadResult);
// Bidirectional streaming: stream of requests, stream of responses
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetUserRequest {
int64 user_id = 1;
}
message GetUserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}The protoc compiler generates typed client stubs and server interfaces:
# Generated server interface (Python)
class UserServiceServicer:
def GetUser(self, request, context):
# request.user_id is a typed int64
user = db.find_user(request.user_id)
return GetUserResponse(user=user)
def ListUsers(self, request, context):
# Server streaming: yield multiple responses
users = db.list_users(page_size=request.page_size)
for user in users:
yield user
# Generated client stub (Python)
channel = grpc.insecure_channel('user-service:50051')
stub = UserServiceStub(channel)
response = stub.GetUser(GetUserRequest(user_id=42))
print(response.user.name) # "Alice" - fully typedgRPC runs on HTTP/2, which provides several advantages over HTTP/1.1:
HTTP/1.1 allows one request per TCP connection at a time. To send 10 concurrent requests, you need 10 connections (or use pipelining, which has head-of-line blocking issues). HTTP/2 multiplexes multiple requests over a single TCP connection using streams.
HTTP/1.1 (6 connections for 6 concurrent requests):
Connection 1: [Request A] ──────────────> [Response A]
Connection 2: [Request B] ──────────────> [Response B]
Connection 3: [Request C] ──────────────> [Response C]
Connection 4: [Request D] ──────────────> [Response D]
Connection 5: [Request E] ──────────────> [Response E]
Connection 6: [Request F] ──────────────> [Response F]
HTTP/2 (1 connection, 6 multiplexed streams):
Connection 1:
Stream 1: [Req A] ────> [Resp A]
Stream 2: [Req B] ──────> [Resp B]
Stream 3: [Req C] ────────> [Resp C]
Stream 4: [Req D] ──> [Resp D]
Stream 5: [Req E] ─────────> [Resp E]
Stream 6: [Req F] ───> [Resp F]
(all interleaved on the same TCP connection)HTTP/2 compresses headers, which is significant because HTTP headers are repetitive across requests (same auth tokens, content types, etc.). HPACK maintains a header table and sends only the differences.
The server can proactively send data to the client before it is requested. In gRPC, this manifests as server streaming RPCs.
The simplest pattern. Client sends one request, server sends one response. Equivalent to a REST API call.
Client ──[GetUser(id=42)]──> Server
Client <──[User{...}]────── ServerClient sends one request, server sends a stream of responses. Useful for large result sets, real-time updates, or long-running operations.
Client ──[ListUsers(page_size=100)]──> Server
Client <──[User 1]─────────────────── Server
Client <──[User 2]─────────────────── Server
Client <──[User 3]─────────────────── Server
Client <──[... end of stream]──────── ServerClient sends a stream of requests, server sends one response. Useful for uploading large data in chunks or aggregating client data.
Client ──[PhotoChunk 1]──> Server
Client ──[PhotoChunk 2]──> Server
Client ──[PhotoChunk 3]──> Server
Client ──[end of stream]──> Server
Client <──[UploadResult{...}]── ServerBoth client and server send streams of messages independently. They can read and write in any order. Useful for chat applications, real-time collaboration, or interactive workflows.
Client ──[Msg A]──> Server
Client <──[Msg 1]── Server
Client ──[Msg B]──> Server
Client ──[Msg C]──> Server
Client <──[Msg 2]── Server
Client <──[Msg 3]── Server| Aspect | gRPC | REST (HTTP/1.1 + JSON) |
|---|---|---|
| Serialization | Binary (protobuf), fast | Text (JSON), slow |
| Payload size | 3-10x smaller | Larger (field names, formatting) |
| Contract | Strongly typed .proto files | OpenAPI/Swagger (optional) |
| Streaming | Native (4 types) | Workarounds (SSE, WebSocket) |
| Code generation | Built-in, multi-language | Third-party tools |
| HTTP version | HTTP/2 (multiplexing) | Usually HTTP/1.1 |
| Browser support | Limited (needs grpc-web proxy) | Native |
| Human readability | Binary (needs tools to inspect) | Text (curl, browser) |
| Tooling | Specialized (grpcurl, BloomRPC) | Universal (curl, Postman) |
Choose gRPC for: Internal service-to-service communication, high-throughput low-latency RPCs, polyglot environments needing strongly typed contracts, streaming use cases.
Choose REST for: Public APIs, browser-facing endpoints, simple CRUD operations, environments where human readability and debuggability are priorities.
Many companies use both: REST for external APIs (consumed by third parties, browsers, mobile apps) and gRPC for internal service communication (where performance matters and both sides of the API are controlled by the same organization).
External clients → REST API Gateway → Internal gRPC services
Browser/Mobile ──REST──> API Gateway ──gRPC──> User Service
──gRPC──> Order Service
──gRPC──> Payment ServiceOne of protobuf's greatest strengths is its backward and forward compatibility model.
// Version 1
message User {
int64 id = 1;
string name = 2;
}
// Version 2 (backward compatible)
message User {
int64 id = 1;
string name = 2;
string email = 3; // NEW: added field with new field number
// Old clients ignore field 3 (they don't know about it)
// New clients reading old data see email as "" (default value)
}`reserved 3;`// Marking removed fields as reserved
message User {
int64 id = 1;
string name = 2;
// string email = 3; // REMOVED
reserved 3; // Prevents accidental reuse
reserved "email"; // Prevents accidental reuse of name
string phone = 4; // New field with NEW number
}gRPC uses a set of standard status codes (similar to HTTP status codes but different):
OK (0) → Success
CANCELLED (1) → Client cancelled the request
INVALID_ARGUMENT (3)→ Client sent bad data (like HTTP 400)
NOT_FOUND (5) → Resource not found (like HTTP 404)
ALREADY_EXISTS (6) → Resource already exists (like HTTP 409)
PERMISSION_DENIED (7) → Not authorized (like HTTP 403)
UNAUTHENTICATED (16) → Not authenticated (like HTTP 401)
RESOURCE_EXHAUSTED (8)→ Rate limited (like HTTP 429)
INTERNAL (13) → Server error (like HTTP 500)
UNAVAILABLE (14) → Service temporarily unavailable (like HTTP 503)
DEADLINE_EXCEEDED (4) → Timeout (no direct HTTP equivalent)gRPC also supports deadlines natively. A client sets a deadline (e.g., "this call must complete within 500ms"), and the deadline propagates through the entire call chain. If Service A calls Service B with a 500ms deadline, and Service B calls Service C, Service C inherits the remaining deadline. This prevents cascading timeouts.
gRPC supports interceptors for cross-cutting concerns, similar to middleware in REST frameworks:
# Server interceptor for logging and authentication
class AuthInterceptor(grpc.ServerInterceptor):
def intercept_service(self, continuation, handler_call_details):
metadata = dict(handler_call_details.invocation_metadata)
token = metadata.get('authorization')
if not validate_token(token):
context.abort(grpc.StatusCode.UNAUTHENTICATED, 'Invalid token')
return continuation(handler_call_details)
# Chain interceptors
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
interceptors=[AuthInterceptor(), LoggingInterceptor(), MetricsInterceptor()]
)`reserved` for removed fields. This shows you understand API evolution.