system-design

gRPC and RPC

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


What RPC is

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.


gRPC

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.


Why it’s fast

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.


The four call types

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.


What it costs

⚖️ 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.


Schema evolution

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


When to use gRPC vs REST vs GraphQL

  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 and service meshes

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.


⚖️ Trade-offs

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

In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

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.


Check yourself

1. Why is gRPC faster than REST/JSON? Two reasons, both from fundamentals. **Protocol Buffers** are binary and schema-based: field names never travel on the wire (just numbers), there's no text parsing or string allocation, and messages are 3–10× smaller and 5–10× faster to decode than JSON. **HTTP/2** multiplexes many concurrent calls over a single long-lived connection with header compression and binary framing, avoiding the per-request handshake and head-of-line blocking that plague HTTP/1.1. Together, at the high call volumes typical of internal service-to-service traffic — where serialization is often the single largest CPU cost — the efficiency difference is substantial, which is exactly why Google runs its internal RPC this way.
2. Why can't browsers use gRPC directly, and what do you do about it? Browsers don't give JavaScript enough control over HTTP/2 frames to speak the raw gRPC wire protocol — they can't manage the trailers and framing gRPC requires. The workarounds are **gRPC-Web**, a variant that a proxy (Envoy has built-in support) translates to and from real gRPC, or **Connect**, a newer protocol designed to be browser-friendly and speak both gRPC and its own HTTP-based format. This limitation is the single biggest reason gRPC is used for internal service-to-service communication and REST/JSON is used at the public, browser-facing edge — even at companies that use gRPC heavily inside.
3. What's the load balancing gotcha with gRPC? gRPC uses long-lived HTTP/2 connections that multiplex many requests. A connection-level (L4) load balancer makes its routing decision once, when the connection is established, and then sends *every* request on that connection to the same backend — so one client's entire call stream lands on one server, and load is badly skewed even though the connections look balanced. You need L7 / gRPC-aware load balancing that distributes individual *requests*, or client-side load balancing where the client knows the backend list and picks per call (common with gRPC, often via a service mesh or a name resolver). This is the same connection-vs-request issue that affects any HTTP/2 traffic.
4. How does protobuf enable safe schema evolution? Fields are identified by a **number**, not a name, and decoders are specified to skip fields whose numbers they don't recognize. So adding a new field with a new number is non-breaking: old services reading a new message simply ignore the unknown field, and new services reading an old message see the new field as absent/default. The rules that preserve this: never reuse a field number (reserve deleted ones so the compiler prevents accidental reuse), and never change a field's number or type. Because of this, two services can deploy independently and interoperate across versions — which is essential in a microservices system where you can't upgrade all services atomically. It's a stronger guarantee than ad-hoc JSON evolution, where behaviour on unknown fields depends on the parser.
5. When would you choose gRPC, REST, and GraphQL respectively? **gRPC** for internal service-to-service communication inside a datacenter, where call volume is high, latency budgets are tight, and both ends are services you control — the binary efficiency, streaming, and compiler-enforced contract all pay off, and the lack of browser support and human-readability don't matter. **REST/JSON** at the public edge and for browser-facing APIs, because every language and tool consumes it with zero setup, it's debuggable with curl, and it gets HTTP caching for free. **GraphQL** where diverse clients need flexible, graph-shaped data and you'd otherwise build a BFF per client. Most large systems use all three: REST at the edge, gRPC internally, GraphQL where client flexibility is the requirement.

Further reading