Call a function on another machine as if it were local. Binary, fast, schema-first — and the right default for service-to-service communication inside a datacenter.
Prerequisites: Serialization, HTTP Versions Time to read: ~20 minutes
Remote Procedure Call: make a call to a remote service look like a local function call.
# Feels local; is actually a network call to another machine
user = user_service.GetUser(GetUserRequest(id=42))
The framework handles serialization, the network, and deserialization, so the developer writes
GetUser(...) and gets a User back — the mechanics are hidden.
🚨 And that hiding is the historical danger. Early RPC frameworks (CORBA, Java RMI, SOAP) pretended remote calls were just like local calls — but they aren’t: they’re 1,000×+ slower, they can fail independently, and they can time out (the 8 fallacies). Treating a network call like a local one produces chatty, fragile systems. Modern RPC (gRPC) succeeds partly by not hiding failure — timeouts, retries, and streaming are first-class.
Google’s RPC framework, built on HTTP/2 and Protocol Buffers. The de-facto standard for internal service communication.
Schema-first — the .proto file is the contract:
syntax = "proto3";
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (stream User); // server streaming
rpc UploadEvents(stream Event) returns (UploadSummary); // client streaming
rpc Chat(stream Message) returns (stream Message); // bidirectional
}
message GetUserRequest { int64 id = 1; }
message User { int64 id = 1; string name = 2; string email = 3; }
You compile this into generated client and server code for Go, Java, Python, C++, etc. 🚨 The generated code is type-safe on both ends — call the wrong method or pass the wrong type and it fails at compile time, not in production.
Two design choices, both traceable to fundamentals:
1. Protocol Buffers (binary). 3–10× smaller than JSON and 5–10× faster to parse. No field names on the wire (just numbers), no text parsing, no string allocation. → Serialization
📐 A user record: ~70 bytes as JSON, ~25 bytes as protobuf. At high internal call volume, the CPU saved on serialization alone is significant — often the single largest cost in a “simple” service.
2. HTTP/2. Multiplexing (many concurrent calls on one connection), header compression, binary framing, and long-lived connections. No per-call handshake. → HTTP Versions
Together: gRPC is dramatically more efficient than REST/JSON for the internal-service case, which is exactly where call volume is highest and latency budgets tightest.
gRPC’s streaming support is a genuine differentiator over REST:
| Type | Shape | Use for |
|---|---|---|
| Unary | 1 request → 1 response | Ordinary calls (like REST) |
| Server streaming | 1 request → stream of responses | Large result sets, live feeds, progress updates |
| Client streaming | Stream of requests → 1 response | Uploading a large dataset, aggregating events |
| Bidirectional | Stream ↔ stream | Chat, real-time collaboration, live coordination |
🚨 Streaming is a real advantage. Server streaming lets you return a million rows without buffering them all, or push live updates without a separate WebSocket layer. This is something REST does poorly.
⚖️ gRPC is not the answer for everything, and the limitations matter.
1. 🚨 No native browser support. Browsers can’t speak raw gRPC (they can’t fully control HTTP/2 frames). You need gRPC-Web with a proxy that translates, or Connect (a newer, browser-friendly protocol). This is the single biggest reason gRPC is internal-only for most systems.
2. Not human-readable. You can’t curl a gRPC endpoint and read the response. Debugging needs
tooling (grpcurl, reflection), and inspecting traffic on the wire shows binary. This friction is
real, especially for debugging in production.
3. A build step and code generation. The .proto toolchain must be integrated into every
service’s build. Not hard, but not zero.
4. Steeper learning curve than REST/JSON, and less universal tooling.
5. Load balancing is different. gRPC uses long-lived HTTP/2 connections, so a connection-level (L4) load balancer sends all of one client’s calls to one server — you need L7/gRPC-aware load balancing or client-side load balancing to distribute requests. This trips people up.
6. Firewalls and proxies sometimes struggle with HTTP/2 and long-lived connections.
Protobuf’s field-number system gives you excellent, safe evolution — the same rules as serialization:
message User {
int64 id = 1;
string name = 2;
string email = 3;
string phone = 4; // NEW: old clients ignore field 4. Non-breaking.
}
🚨 The rules: add new fields with new numbers (✅), never reuse a field number (reserve deleted ones), never change a field’s type or number. Because fields are identified by number and decoders skip unknown fields, old and new clients interoperate — which is what makes independent deployment of services safe. This is a genuine advantage over ad-hoc JSON evolution. → Versioning
| REST | GraphQL | gRPC | |
|---|---|---|---|
| Format | JSON (text) | JSON | Protobuf (binary) |
| Speed | Baseline | Baseline | 3–10× smaller, 5–10× faster parse |
| Browser | ✅ Native | ✅ | ❌ (needs gRPC-Web) |
| Streaming | ✗ (SSE separate) | Subscriptions | ✅ First-class, all directions |
| Human-readable | ✅ | ✅ | ❌ |
| Contract | OpenAPI (optional) | Schema | Proto (mandatory) |
| Best for | Public APIs | Flexible clients | Internal service-to-service |
🎙️ The standard recommendation: “gRPC for service-to-service inside the datacenter — the binary format and HTTP/2 make it far more efficient at the call volumes where it matters, and the proto schema gives us a compiler-enforced contract. REST/JSON at the public edge, because every client speaks it and it’s debuggable. GraphQL where clients need flexible shapes.”
This layered answer — REST edge, gRPC internal — is close to the industry default, and stating it is a strong signal.
gRPC pairs naturally with a service mesh: the mesh handles retries, timeouts, mTLS, load balancing, and tracing for gRPC calls, and gRPC’s structured metadata makes it easy. Some setups use proxyless gRPC (gRPC clients speak the mesh’s xDS control plane directly, no sidecar). Worth knowing as a currency point.
| Choice | Gain | Cost |
|---|---|---|
| gRPC over REST (internal) | Much faster/smaller; type-safe contract; streaming | Binary (not debuggable); build step; no browser; L7 LB needed |
| Protobuf | Compact, fast, safe evolution | Opaque on the wire; needs the schema to decode |
| HTTP/2 multiplexing | No per-call handshake; concurrent calls | Connection-level LB doesn’t distribute requests |
| Streaming | Efficient large results and real-time in one protocol | More complex than request/response |
| gRPC-Web / Connect | Browser access | A proxy or a different protocol |
1. Measure the size and speed difference. Serialize 100,000 realistic records as JSON and as protobuf. Compare total bytes and parse time. The numbers are usually more dramatic than expected — and they’re the whole argument for gRPC internally.
2. Build a streaming service. Implement server streaming — a method that returns a million rows as a stream. Note that the client processes rows as they arrive without buffering everything, and compare to how you’d paginate the same thing over REST. That’s gRPC’s streaming advantage, made concrete.
3. Hit the browser wall. Try to call a gRPC service directly from browser JavaScript. It won’t work. Then set up gRPC-Web (or Connect) with a proxy and watch it work. This makes the “internal only” limitation concrete.
4. Break and evolve a schema. Add a field to a proto message (new number) and confirm old clients still work. Then reuse a field number and watch old clients silently misread data. That’s why the “never reuse a number” rule exists.