gRPC and Protocol Buffers
TL;DR
gRPC is Google's open-source RPC framework that pairs Protocol Buffers (a binary serialization format) with HTTP/2 to deliver fast, type-safe, cross-language communication. You define your service in a .proto file, and gRPC generates client and server code in any supported language. The performance gain comes from Protobuf's compact binary encoding (5-10x smaller than JSON) and HTTP/2's multiplexing — not from "skipping HTTP."
What Is gRPC?
gRPC stands for gRPC Remote Procedure Call (yes, it's recursive). Google open-sourced it in 2015, but it's built on technology Google had been using internally for over a decade. Today it's a graduated CNCF project used by companies like Netflix, Dropbox, Slack, Cisco, and CoreOS.
Think of gRPC as the modern answer to the question: "How do I make RPC that's fast, type-safe, and works across any programming language?"
The answer has three parts:
- Protocol Buffers — A binary serialization format that's the "language" services speak
- HTTP/2 — The transport protocol that carries the data
- Code Generation — Tooling that auto-generates client and server stubs from a single definition file
Here's what each one looks like in practice.

Protocol Buffers: The Single Source of Truth
Protocol Buffers (or Protobuf) is a language-neutral, platform-neutral way to define data structures and service interfaces. You write a .proto file, and it becomes the contract between every service that communicates.
Here's a real-world example — a Ticketmaster-like service:
syntax = "proto3";
package ticketmaster;
// The service definition — what functions are available
service TicketService {
rpc GetEvent(GetEventRequest) returns (Event);
rpc CreateBooking(CreateBookingRequest) returns (Booking);
rpc GetAvailableTickets(GetTicketsRequest) returns (TicketList);
rpc CancelBooking(CancelBookingRequest) returns (CancelResponse);
}
// Message definitions — the data structures
message GetEventRequest {
string event_id = 1;
}
message Event {
string event_id = 1;
string name = 2;
string venue = 3;
int64 date_unix = 4;
repeated Ticket available_tickets = 5;
}
message Ticket {
string ticket_id = 1;
string section = 2;
int32 row = 3;
int32 seat = 4;
float price = 5;
}
message CreateBookingRequest {
string event_id = 1;
string user_id = 2;
repeated string ticket_ids = 3;
}
message Booking {
string booking_id = 1;
string event_id = 2;
string user_id = 3;
repeated Ticket tickets = 4;
float total_price = 5;
string status = 6;
}
message GetTicketsRequest {
string event_id = 1;
string section = 2; // optional filter
float max_price = 3; // optional filter
}
message TicketList {
repeated Ticket tickets = 1;
}
message CancelBookingRequest {
string booking_id = 1;
}
message CancelResponse {
bool success = 1;
string message = 2;
}
This single file defines everything about the service: what functions it exposes, what data each function expects, and what it returns. It's a contract that both the client and server must follow.
Notice those numbers after each field (= 1, = 2, etc.)? Those are field numbers, not default values. They're used for binary encoding — Protobuf doesn't send field names over the wire, only these compact numbers. This is one reason the binary format is so much smaller than JSON.
Code Generation: One .proto, Every Language
Here's where the magic happens. From that single .proto file, gRPC's compiler (protoc) generates ready-to-use code in any supported language:
# Generate Go server and client code
protoc --go_out=. --go-grpc_out=. ticket_service.proto
# Generate Java client code
protoc --java_out=. --grpc-java_out=. ticket_service.proto
# Generate Python client code
protoc --python_out=. --grpc_python_out=. ticket_service.proto
After running these commands, you get:
- Go server: A
TicketServiceServerinterface you implement with your business logic - Java client: A
TicketServiceStubclass that lets Java code callgetEvent()as if it were local - Python client: A
TicketServiceStubclass that does the same for Python
The generated code handles all the serialization, deserialization, and network communication. Your Go server and Java client speak the same binary protocol without either team writing any serialization logic.
Using the generated code on the server side (Go):
type ticketServer struct {
pb.UnimplementedTicketServiceServer
}
func (s *ticketServer) GetEvent(ctx context.Context, req *pb.GetEventRequest) (*pb.Event, error) {
// Your business logic — query database, build response
event, err := db.FindEvent(req.EventId)
if err != nil {
return nil, status.Errorf(codes.NotFound, "event %s not found", req.EventId)
}
return &pb.Event{
EventId: event.ID,
Name: event.Name,
Venue: event.Venue,
}, nil
}
Using the generated code on the client side (Java):
// Create a channel and stub
ManagedChannel channel = ManagedChannelBuilder
.forAddress("ticket-service", 50051)
.usePlaintext()
.build();
TicketServiceGrpc.TicketServiceBlockingStub stub =
TicketServiceGrpc.newBlockingStub(channel);
// Call the remote function — looks like a local call!
Event event = stub.getEvent(
GetEventRequest.newBuilder()
.setEventId("123")
.build()
);
System.out.println("Event: " + event.getName());
The Go server and Java client were built by different teams, in different languages, possibly in different time zones. But they communicate flawlessly because they share the same .proto contract.
Type Safety: Catch Bugs at Compile Time
This is one of gRPC's biggest advantages over REST with JSON. With JSON APIs:
// Server sends this
{"event_id": "123", "name": "Taylor Swift Concert", "date": "2025-09-15"}
// Client expects this (oops — "date" vs "event_date")
{"event_id": "123", "name": "Taylor Swift Concert", "event_date": "2025-09-15"}
This mismatch silently fails at runtime. The client gets null for event_date and you discover the bug when a user reports it in production.
With Protocol Buffers, the .proto file is the contract. If the server changes a field name, the client's generated code won't compile until it's updated. You catch mismatches at compile time, not in production at 3 AM.
| Aspect | JSON (REST) | Protocol Buffers (gRPC) |
|---|---|---|
| Type checking | Runtime (if at all) | Compile time |
| Field names | Strings — typos compile fine | Generated constants — typos don't compile |
| Missing fields | Silently null/undefined |
Compiler warning or default values |
| Schema | Optional (OpenAPI/Swagger) | Required (.proto file) |
| Cross-language | Every team writes their own models | Generated from same source |
Why gRPC Is Faster (And Why Most People Get This Wrong)
This is the most commonly misunderstood aspect of gRPC, and getting it right in an interview will impress your interviewer.
You'll often hear people say: "gRPC is faster because it doesn't use HTTP" or "gRPC bypasses HTTP for direct binary communication."
This is wrong. gRPC absolutely uses HTTP. Specifically, it uses HTTP/2.
The performance advantage comes from two distinct sources:
1. Protocol Buffers: Binary vs. Text Serialization
Consider this event data as JSON:
{
"event_id": "evt_123456",
"name": "Taylor Swift | The Eras Tour",
"venue": "SoFi Stadium, Los Angeles",
"date_unix": 1726444800,
"available_tickets": [
{"ticket_id": "tkt_001", "section": "Floor", "row": 12, "seat": 5, "price": 450.00},
{"ticket_id": "tkt_002", "section": "Floor", "row": 12, "seat": 6, "price": 450.00}
]
}
This JSON payload is about 350 bytes as text. Every field name is spelled out as a string. Every number is encoded as text characters.
The same data in Protocol Buffers is roughly 80-100 bytes — a binary blob where field names are replaced by 1-byte field numbers, integers are stored as actual integers (not ASCII digits), and there's no whitespace or formatting.
That's 3-5x smaller for this example, and the difference grows with larger payloads. For high-volume internal APIs exchanging millions of messages per second, this adds up to significant bandwidth and CPU savings.
2. HTTP/2: Multiplexing and Header Compression
Traditional REST APIs typically run over HTTP/1.1, which has a fundamental limitation: one request per TCP connection at a time. If you need to make 10 API calls, you either wait for each one to finish (slow) or open 10 separate connections (expensive).
gRPC uses HTTP/2, which provides:
| Feature | HTTP/1.1 | HTTP/2 |
|---|---|---|
| Requests per connection | One at a time | Many in parallel (multiplexing) |
| Header format | Text, repeated every request | Binary, compressed (HPACK) |
| Connection overhead | New connection per request (often) | Single long-lived connection |
| Server push | Not supported | Supported |
With HTTP/2 multiplexing, a single TCP connection can carry hundreds of concurrent requests and responses, interleaved as binary frames. This dramatically reduces connection overhead, especially for microservices that make many small calls to each other.
The Combined Effect
It's the combination that makes gRPC fast:
Traditional REST: JSON text (big) → HTTP/1.1 (one-at-a-time) → Slow
gRPC: Protobuf binary (small) → HTTP/2 (multiplexed) → Fast
The accurate way to describe it: "gRPC is faster than REST's typical JSON-over-HTTP/1.1 approach because of binary serialization (Protocol Buffers) and HTTP/2 multiplexing." Not "gRPC is faster than HTTP."
Interview Tip
If you say "gRPC is faster because it doesn't use HTTP," a knowledgeable interviewer will immediately flag this. The correct framing is: "gRPC leverages HTTP/2 for multiplexing and header compression, combined with Protocol Buffers for compact binary serialization. The performance gain comes from both the transport efficiency and the serialization format." This shows you actually understand the stack rather than just parroting a soundbite.
Binary Serialization: The Broader Landscape
Protocol Buffers isn't the only binary serialization format. Here's how it compares to alternatives:
| Format | Created By | Key Traits | Used In |
|---|---|---|---|
| Protocol Buffers | Schema-required, compact, field numbers | gRPC, Google internal | |
| Apache Thrift | Multiple serialization formats, multiple transports | Facebook internal, Hadoop ecosystem | |
| Avro | Apache | Schema evolution, JSON-defined schemas, no field numbers | Hadoop, Kafka, data pipelines |
| BSON | MongoDB | Binary JSON — supports more types (dates, binary data) | MongoDB wire protocol |
| MessagePack | Sadayuki Furuhashi | Schema-less binary JSON — compact but no type safety | Redis, Fluentd |
| FlatBuffers | Zero-copy deserialization — access data without parsing | Games, mobile apps |
Why not just use BSON everywhere? BSON (Binary JSON) is a reasonable middle ground — it's binary but doesn't require a schema. However, it doesn't give you code generation or compile-time type safety. It's great for databases (MongoDB uses it natively) but not ideal for service contracts.
Why not just use JSON with gzip? Compressed JSON gets close to Protobuf's size, but compression and decompression use CPU. Protobuf is already small before compression, so it wins on both CPU and bandwidth. For a few API calls, this doesn't matter. For millions of internal calls per second, it absolutely does.
It's worth noting that some systems use entirely custom encodings — Prometheus, for example, uses a custom textual format for metrics exposition rather than JSON or Protobuf. The right format depends on the use case.
Protobuf Field Numbers and Backward Compatibility
Those field numbers in .proto files (= 1, = 2, etc.) are critical for backward compatibility — one of Protobuf's killer features in a microservices world.
message Event {
string event_id = 1;
string name = 2;
string venue = 3;
int64 date_unix = 4;
// Added in v2 — old clients won't know about this field
string category = 5;
// Added in v3
bool is_sold_out = 6;
}
Here's how backward compatibility works:
- Adding a field (like
category = 5): Old clients that don't know about field 5 simply ignore it. New clients that expect it get a default value if the old server doesn't send it. No breakage. - Removing a field: Old clients that still send it won't break the new server — unknown fields are ignored. You should mark removed field numbers as
reservedso they're never accidentally reused. - Changing a field number: Never do this. Field number 2 means "name" forever. Changing it breaks everything.
- Renaming a field: Safe! Field names are only used in generated code, not on the wire. The wire only uses field numbers.
// This is SAFE — renaming doesn't change the wire format
message Event {
string event_id = 1;
string event_name = 2; // was "name" — field number 2 is unchanged
}
// This is DANGEROUS — never reuse or change field numbers
message Event {
string event_id = 1;
string name = 3; // WRONG — changed from 2 to 3, breaks all existing clients
}
This means you can evolve your API without coordinating simultaneous deployments of every service. In a microservices environment with 50+ services, this is invaluable. You deploy the server update, then client teams update on their own schedule.
Apache Thrift: The Other Major RPC Framework
Before gRPC dominated the conversation, Facebook created Apache Thrift (2007, open-sourced to Apache). Thrift is similar to gRPC in many ways:
| Aspect | gRPC | Apache Thrift |
|---|---|---|
| Created by | Google (2015) | Facebook (2007) |
| Serialization | Protocol Buffers only | Multiple formats (binary, compact, JSON) |
| Transport | HTTP/2 only | Multiple (TCP, HTTP, pipes) |
| Streaming | Built-in (4 patterns) | Limited streaming support |
| Language support | 10+ languages | 20+ languages |
| IDL | .proto files |
.thrift files |
| Ecosystem | Huge (CNCF, Cloud Native) | Smaller, more niche |
| HTTP/2 features | Multiplexing, flow control | Depends on transport choice |
Thrift's flexibility (multiple serialization formats, multiple transports) can be both an advantage and a curse — more choices mean more decisions. gRPC's opinionated "Protobuf + HTTP/2 only" approach means fewer decisions and better tooling.
In practice, gRPC has won the modern RPC mindshare. Unless you're working at Facebook (now Meta) or in the Hadoop ecosystem, you're more likely to encounter gRPC.
When Protobuf Matters (And When It Doesn't)
Protocol Buffers add complexity — you need .proto files, a compilation step, and tooling. When is that worth it?
| Scenario | Use Protobuf/gRPC? | Why |
|---|---|---|
| Millions of internal API calls/sec | Yes | Binary encoding saves massive bandwidth and CPU |
| Polyglot microservices (Go + Java + Python) | Yes | One .proto generates all client/server code |
| Mobile clients on slow networks | Yes | Smaller payloads = faster load times, less data usage |
| Public API for third-party developers | No | JSON is universally understood; Protobuf adds friction |
| Simple CRUD app with 3 endpoints | No | Overkill — REST with JSON is simpler |
| Prototyping / early startup | No | Move fast, JSON is good enough |
| Real-time streaming requirements | Yes | gRPC streaming is built-in and battle-tested |
| Strict cross-team API contracts | Yes | Compile-time type safety prevents integration bugs |
The rule of thumb: if you control both sides of the communication and you need performance or type safety, use gRPC. If your API needs to be consumed by the general public, use REST with JSON.
Interview Tip
When discussing internal service communication in a system design interview, you don't need to write out .proto definitions. Simply say: "The recommendation service and the booking service communicate over gRPC with Protocol Buffers for type safety and performance." That single sentence shows you know the technology and when to apply it.
Interview Expectations: Junior vs. Senior
- Junior/Mid-level: Knows gRPC is "Google's fast API framework" and uses
.protofiles. May incorrectly state that "gRPC is fast because it avoids HTTP." - Senior/Staff: Correctly identifies that gRPC gets its performance from binary serialization via Protocol Buffers (smaller payloads, faster CPU parsing than JSON text) and HTTP/2 (multiplexing). Leverages code generation to eliminate entire classes of serialization bugs between microservices.