← back

Networking & Communication

gRPC and Protocol Buffers

High-performance RPC framework using binary serialization. When to choose gRPC over REST, and how to design service contracts with Protocol Buffers.

gRPC and 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 (Protobuf)

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.

Defining a Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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;
}

Binary Serialization

The key insight: protobuf encodes data using field numbers (not field names) and a compact binary format. Compare:

1
2
3
4
5
6
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.

How Field Numbers Work

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.

1
2
3
4
5
6
7
8
9
10
11
12
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: Remote Procedure Calls

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.

Service Definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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;
}

Code Generation

The protoc compiler generates typed client stubs and server interfaces:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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 typed

HTTP/2 Multiplexing

gRPC runs on HTTP/2, which provides several advantages over HTTP/1.1:

Multiplexing

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

Header Compression (HPACK)

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.

Server Push

The server can proactively send data to the client before it is requested. In gRPC, this manifests as server streaming RPCs.

Four Types of RPCs

Unary (Request-Response)

The simplest pattern. Client sends one request, server sends one response. Equivalent to a REST API call.

1
2
Client ──[GetUser(id=42)]──> Server
Client <──[User{...}]────── Server

Server Streaming

Client sends one request, server sends a stream of responses. Useful for large result sets, real-time updates, or long-running operations.

1
2
3
4
5
Client ──[ListUsers(page_size=100)]──> Server
Client <──[User 1]─────────────────── Server
Client <──[User 2]─────────────────── Server
Client <──[User 3]─────────────────── Server
Client <──[... end of stream]──────── Server

Client Streaming

Client sends a stream of requests, server sends one response. Useful for uploading large data in chunks or aggregating client data.

1
2
3
4
5
Client ──[PhotoChunk 1]──> Server
Client ──[PhotoChunk 2]──> Server
Client ──[PhotoChunk 3]──> Server
Client ──[end of stream]──> Server
Client <──[UploadResult{...}]── Server

Bidirectional Streaming

Both 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.

1
2
3
4
5
6
Client ──[Msg A]──> Server
Client <──[Msg 1]── Server
Client ──[Msg B]──> Server
Client ──[Msg C]──> Server
Client <──[Msg 2]── Server
Client <──[Msg 3]── Server

When gRPC Beats REST

AspectgRPCREST (HTTP/1.1 + JSON)
SerializationBinary (protobuf), fastText (JSON), slow
Payload size3-10x smallerLarger (field names, formatting)
ContractStrongly typed .proto filesOpenAPI/Swagger (optional)
StreamingNative (4 types)Workarounds (SSE, WebSocket)
Code generationBuilt-in, multi-languageThird-party tools
HTTP versionHTTP/2 (multiplexing)Usually HTTP/1.1
Browser supportLimited (needs grpc-web proxy)Native
Human readabilityBinary (needs tools to inspect)Text (curl, browser)
ToolingSpecialized (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.

The Pragmatic Approach

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).

1
2
3
4
5
External clients → REST API Gateway → Internal gRPC services

Browser/Mobile ──REST──> API Gateway ──gRPC──> User Service
                                     ──gRPC──> Order Service
                                     ──gRPC──> Payment Service

Backward Compatibility

One of protobuf's greatest strengths is its backward and forward compatibility model.

Safe Changes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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)
}

Rules for Compatibility

  1. Never change a field number. Field numbers are the identity of each field in the binary format.
  2. Never reuse a field number. If you remove field 3, reserve it: `reserved 3;`
  3. Adding fields is always safe. Old clients ignore unknown fields. New clients use defaults for missing fields.
  4. Removing fields is safe if you reserve the number. Old clients sending removed fields are harmless (new code ignores them).
  5. Renaming fields is safe. Names are not in the wire format.
  6. Changing types is dangerous. Some type changes are compatible (int32 → int64), others are not (string → int32).
1
2
3
4
5
6
7
8
9
// 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
}

Error Handling

gRPC uses a set of standard status codes (similar to HTTP status codes but different):

1
2
3
4
5
6
7
8
9
10
11
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.

Interceptors (Middleware)

gRPC supports interceptors for cross-cutting concerns, similar to middleware in REST frameworks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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()]
)

Interview Tips

  • *Explain why gRPC exists.* Start with the limitations of REST+JSON for internal service communication: verbose payloads, no streaming, weak contracts. Then present gRPC as the solution.
  • Describe protobuf's wire format briefly. Field numbers instead of field names, varint encoding for integers. This shows you understand why it is faster, not just that it is.
  • Mention HTTP/2 multiplexing. This is a key advantage over REST on HTTP/1.1. One connection, multiple concurrent streams.
  • Know the four RPC types. Unary, server streaming, client streaming, bidirectional. Give a concrete use case for each.
  • Discuss backward compatibility rules. Never reuse field numbers. Always add new fields with new numbers. Use `reserved` for removed fields. This shows you understand API evolution.
  • Address gRPC's weaknesses. Poor browser support (needs grpc-web), harder to debug (binary format), steeper learning curve. These are not dealbreakers but show balanced thinking.
  • Propose the hybrid approach. REST for external/public APIs, gRPC for internal service-to-service. This is the most common real-world architecture and demonstrates pragmatism.