gRPC β Part 1: Foundations & Core Concepts
What is this?
Imagine you're on one floor of a big office and you need a number from a colleague two floors up. You could walk down, find their desk, explain what you need, wait, and walk back. Or you could pick up the phone, dial their extension, ask, and get the answer in seconds β as if they were sitting right next to you. The distance is still there; it just stopped feeling like distance.
gRPC is that phone extension for your services. You call a function β getUser(123) β and it runs on a completely different machine, maybe in another data center, and hands you back a result as if it were a plain local function call. No URLs to assemble, no JSON to hand-parse, no guessing what shape the response will be. You wrote the function signature once in a shared contract, both sides agreed to it, and the wire details vanished underneath you. That "calling a remote function as if it were local" idea is the RPC in gRPC β Remote Procedure Call β and we cover the general pattern in the RPC chapter; gRPC is Google's industrial-strength take on it.
The one-line idea: gRPC lets one service call a method on another service as if it were a local function β you define the contract once in a
.protofile, and gRPC generates the typed client and server code, serializes everything as compact binary over HTTP/2, and hides the network.
That contract-first, code-generated, binary-over-HTTP/2 combination is the whole personality of gRPC. We'll reuse the phone-extension picture across all four parts. Let's earn every piece of it.
1. What gRPC actually is
gRPC is a high-performance, open-source RPC framework that Google released in 2015 (it grew out of an internal system called Stubby that Google had run for years). Three ideas snap together to make it work:
- Protocol Buffers (protobuf) β a binary, schema-first serialization format. You declare your messages and methods in a
.protofile, and that file is the contract. - HTTP/2 as the transport β which brings multiplexing, bidirectional streams, and header compression (we'll dig into why that matters in Part 2).
- Code generation β a compiler reads your
.protoand spits out a typed client stub and a server skeleton in your language. You fill in the server logic and call the client; the plumbing between them is generated.
The payoff of that generated stub is what makes gRPC feel different from hand-rolling HTTP calls: you don't write serialization code, you don't write URL builders, and the types are enforced by your compiler. If you rename a field in the .proto and forget to update a caller, the build breaks β long before a single request goes out.
People reach for gRPC mainly when they care about speed and strict contracts. Compared with a JSON-over-HTTP/1.1 API, you'll typically see meaningfully lower latency, smaller payloads on the wire, and far better throughput on a busy connection β because the data is compact binary and many calls share one connection instead of each paying for its own setup. (Treat those as "directionally large wins," not lab-exact numbers β the real figure depends entirely on your payloads and traffic.)
2. Why REST often isn't enough for service-to-service
REST is wonderful, and you should not rip it out of your public API. For a browser, a mobile app, or a third-party developer poking at your endpoints with curl, REST's human-readable JSON and universal tooling are exactly right. We cover it in its own chapter, and gRPC is not its replacement there.
The friction shows up behind the wall, where your own services call each other thousands of times a second. That's where REST starts charging you tolls:
| The internal pain | What REST + JSON costs you | What gRPC does instead |
|---|---|---|
| Very high call volume | New connection setup overhead, request queuing on HTTP/1.1 | One HTTP/2 connection, multiplexed across many calls |
| Need a strict contract | JSON is untyped β a typo'd field fails silently at runtime | .proto schema, checked at compile time |
| Real-time data flow | Bolt on SSE or WebSockets yourself | Streaming is a first-class RPC type |
| Payload efficiency | Verbose text, re-parsed every hop | Compact binary protobuf |
| Many calls at once | Head-of-line blocking, connection pools to manage | HTTP/2 multiplexing on a single connection |
The thread tying these together: REST optimizes for reach and readability, gRPC optimizes for efficiency and rigor between trusted peers. Different jobs.
π‘ A useful mental rule: the edge speaks REST/GraphQL; the interior speaks gRPC. Browsers and partners hit a REST or GraphQL gateway, and inside your mesh the services chatter over gRPC. Many large systems run exactly this split.
3. Where gRPC shines β and where it hurts
Pick the tool for the terrain.
gRPC is a great fit for:
- Internal microservices β the canonical case: low-latency, high-throughput service-to-service calls.
- Backend-to-backend plumbing β cache invalidation, fan-out to downstream services, control-plane chatter.
- ML / AI serving β model inference and feature lookups, where every millisecond and byte counts.
- Real-time pipelines β metrics, events, telemetry, where streaming is native.
- Polyglot shops β one
.protogenerates a Go client, a Python server, a Java caller, all guaranteed to agree.
gRPC is awkward for:
- Browser-to-server traffic. Browsers can't speak raw gRPC β they don't give JavaScript the low-level HTTP/2 frame control gRPC needs. You need gRPC-Web plus a translating proxy (Envoy), which adds a moving part and clips some features (notably client-streaming). This is the single most important "gotcha" to remember: gRPC is brilliant service-to-service and clumsy browser-to-service. We'll come back to it in the interview chapter.
- Public, third-party APIs β REST's discoverability and zero-setup tooling usually win.
- Dead-simple CRUD β if a tiny JSON endpoint does the job, gRPC's codegen and toolchain may be overkill.
- Teams with no gRPC muscle β the learning curve (protobuf, codegen, debugging binary) is real; don't adopt it the week before a deadline.
4. The four RPC types β the heart of gRPC
This is the part worth memorizing, because "which streaming mode?" is a constant interview question. gRPC gives you four call shapes, and they differ only in which side gets to send a stream of messages instead of a single one.
Walk through each with a concrete use case:
Unary β one request, one response. The plain phone call. GetUser(user_id) β UserProfile. This is your everyday CRUD and lookups, and it's what REST does too.
Server streaming β one request, a stream of responses. You ask once, the server keeps talking. SubscribeToPrices("AAPL") β stream<PriceUpdate> β subscribe to a stock symbol and updates flow until you hang up. Great for live feeds, notifications, and progress updates.
Client streaming β a stream of requests, one response. You keep sending, the server answers once at the end. UploadFile(stream<Chunk>) β UploadResult β push a big file in chunks, get one confirmation. Good for uploads, telemetry batches, log shipping.
Bidirectional streaming β both sides stream independently over the same call. Chat(stream<Message>) β stream<Message> β you talk, the server talks, neither waits its turn. This is where HTTP/2's full-duplex nature earns its keep: video/audio, collaborative editing, gaming, live chat.
Here's the timeline view of all four sharing one connection:
The senior instinct: default to unary. Streaming is powerful but it's also stateful, harder to load-balance, and harder to debug. Reach for it only when the data genuinely flows continuously.
5. Protocol Buffers β the contract
Protobuf is the schema language that makes gRPC's "define it once" promise real. A .proto file is language-neutral, binary, and schema-driven β it describes your data and your methods, and it's the single source of truth both client and server compile against.
Here's a tiny but complete contract:
syntax = "proto3";
package user;
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
message GetUserRequest {
int64 user_id = 1; // the "= 1" is a FIELD NUMBER, not a value
}
message GetUserResponse {
int64 user_id = 1;
string name = 2;
string email = 3;
}
From this one file, protoc generates the typed UserService client stub and server skeleton in whatever languages you target. Your client code becomes a one-liner: response = stub.GetUser(GetUserRequest(user_id=123)).
The detail that trips everyone up at first: the = 1, = 2, = 3 are field numbers, not default values. They're how protobuf identifies fields on the wire β the binary format stores field number 2, not the string "name". This is the secret to gRPC's two superpowers:
- Compactness. Field names never travel; only their numbers do. That's a big chunk of why protobuf is so much smaller than JSON, which repeats every key as text in every message.
- Backward compatibility. Because identity lives in the number, you can rename a field freely, and you can add new fields (with new numbers) without breaking old code β old clients simply ignore numbers they don't recognize. The golden rule that follows: never reuse or renumber a field number. (We'll do a full safe-evolution walkthrough in the interview chapter.)
A few more protobuf essentials worth knowing:
- Scalar types:
int32/int64,float/double,bool,string,bytes, plusenum. - Lists:
repeated string tags = 4;gives you an array. - Nested & composite: messages can contain other messages and enums.
- proto3 quirks:
requiredwas removed (it caused painful evolution problems); useoptionalwhen you need to distinguish "unset" from "zero," andoneoffor mutually exclusive fields.
6. How a call actually flows
Let's trace one unary call end to end, so the layers click into place.
- Your code calls the generated method β
stub.GetUser(...)β like any local function. - The stub serializes the request into compact binary protobuf.
- The channel (a managed, reusable connection) sends it over HTTP/2.
- The server's channel receives the bytes and the skeleton deserializes them back into a typed object.
- Your handler runs the actual business logic.
- The response is serialized and sent back the same way.
- The client stub deserializes it and returns it from the function call β the network never showed its face.
That "the network never showed its face" is the entire point. From your code's perspective, you called a function and got a value. Everything between steps 2 and 6 is gRPC doing the unglamorous work so you don't have to.
Key takeaways
- gRPC lets you call a remote method like a local function β define the contract once in a
.proto, and codegen produces the typed client stub and server skeleton. - It's built from three pieces: Protocol Buffers (binary, schema-first), HTTP/2 transport, and generated stubs in many languages.
- It shines service-to-service (low latency, strict typed contracts, polyglot) and is awkward browser-to-server (needs gRPC-Web + a proxy) β the edge speaks REST/GraphQL, the interior speaks gRPC.
- There are four RPC types β unary, server-streaming, client-streaming, bidirectional β differing only in which side gets to stream. Default to unary; stream only when data truly flows.
- Protobuf field numbers (
= 1,= 2) are wire identifiers, not values β they're the source of both gRPC's compactness and its backward compatibility, and you must never reuse them.
Next up, Part 2 opens the box: how HTTP/2 multiplexing actually works, why protobuf beats JSON on the wire, the four streaming modes in code, and the resilience features β deadlines, retries, load balancing β that make gRPC production-ready.