Skip to content

RPC & gRPC

REST asks you to think in nouns and verbs. RPC — Remote Procedure Call — asks you to think in functions. The goal is older and more direct than REST: make calling code on another machine look and feel as much as possible like calling a function in your own process. You write user := GetUser(42) and somewhere underneath, that turns into bytes on a wire, a call on a remote server, and a value coming back — and ideally you barely notice.

The RPC model: hide the network behind a function call

Section titled “The RPC model: hide the network behind a function call”

The mechanism is a stub (or client) generated for you. You call a normal-looking method; the stub serializes the arguments, sends them, waits, and deserializes the reply.

YOUR CODE THE WIRE REMOTE SERVICE
───────── ──────── ──────────────
GetUser(42) ──► [client stub serializes] ──► [server stub deserializes] ──► GetUser(42)
user ◄──────── [client stub deserializes] ◄── [server stub serializes] ◄──── return user

This is wonderfully ergonomic. It is also the source of RPC’s oldest danger: by hiding the network, it tempts you to forget the network is there — that the call can be slow, can fail, can be retried. The fallacies of distributed computing are essentially a list of the lies a too-transparent RPC abstraction encourages you to believe.

The dominant RPC framework today is gRPC. It is worth understanding as three decisions stacked together, because each one is a deliberate trade.

1. Protocol Buffers (protobuf): a schema and a binary format

Section titled “1. Protocol Buffers (protobuf): a schema and a binary format”

You define your service and its messages in a .proto file — a strict, typed contract:

message GetUserRequest { int32 id = 1; }
message User { int32 id = 1; string name = 2; string email = 3; }
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}

From this single file, gRPC generates client and server code in many languages. The contract lives in a file the compiler enforces — not in prose documentation that drifts. On the wire, protobuf is binary, identifying fields by number (the = 1, = 2) rather than by repeating field names as JSON does. The result is dramatically smaller and faster to parse than JSON text.

What does this buy us, and what does it cost? It buys type safety across the network and compact, fast payloads. It costs human readability — you can’t curl an endpoint and eyeball the response; you need tooling to decode binary protobuf — and it adds a build step (code generation) to your pipeline.

gRPC runs over HTTP/2, which gives it two things REST-over-HTTP/1.1 historically lacked: multiplexing (many concurrent calls share one connection without head-of-line blocking) and native streaming.

Because of HTTP/2, a gRPC method isn’t limited to one-request-one-response. It supports:

Unary: client → 1 request ........ server → 1 response
Server streaming: client → 1 request ........ server → many responses (a stream)
Client streaming: client → many ........ server → 1 response
Bidirectional: client ⇄ server, both stream simultaneously

This makes gRPC a natural fit for things like live feeds, telemetry, and chat backends between services — workloads that are awkward to express in plain REST.

RPC, and gRPC specifically, shines in a recognizable setting: internal, service-to-service communication where you control both ends.

  • Low latency, high volume. Binary protobuf over multiplexed HTTP/2 is materially faster and lighter than JSON over HTTP/1.1. When two services chat thousands of times a second, that difference compounds. (Pair this intuition with latency, throughput & the numbers.)
  • Strong typing and codegen. The .proto is a single source of truth; a breaking change shows up as a compile error in the client, not a 2 a.m. production surprise.
  • Streaming. First-class bidirectional streams without bolting on a second protocol.

This is why gRPC is the common backbone behind the public edge: a REST or GraphQL front door for the outside world, gRPC between your microservices internally.

The ergonomics are not free, and the costs are the mirror image of REST’s strengths.

  • Coupling. Client and server share a generated contract and often a build pipeline. They evolve together; an external partner can’t just point a browser at it. RPC trades REST’s loose, universal contract for a tight, efficient one.
  • Tooling and ecosystem friction. Browsers can’t speak raw gRPC directly (gRPC-Web exists as a proxied workaround). Debugging binary traffic needs special tools. Caching, which REST gets from HTTP almost for free, must be built deliberately.
  • The transparency trap. Because a remote call looks like a local one, teams forget to design for partial failure, timeouts, and retries. A local function never times out; an RPC always can.

How do two services you control talk as cheaply and safely as possible? RPC answers: pretend the network isn’t there — generate typed stubs from a shared contract and let a function call carry the weight. gRPC sharpens this with binary protobuf and HTTP/2 streaming, buying speed and type safety at the cost of tight coupling, harder tooling, and the ever-present temptation to forget that the network can still fail.

  1. What problem is RPC fundamentally trying to solve, and how does a generated stub achieve it?
  2. Name the three stacked decisions inside gRPC (schema/format, transport, call shapes) and what each one buys.
  3. Why is binary protobuf both faster than JSON and harder to debug?
  4. Give two concrete reasons internal microservices often prefer gRPC while the public edge stays REST or GraphQL.
  5. Explain “the transparency trap” — why does making a remote call look local lead to bugs?