Loading...
Loading...
RESTful APIs, Protocol Buffers, and when to use gRPC over REST
Services now register on boot, heartbeat every few seconds, and let callers watch a cached endpoint list, retrying once onto the next healthy instance when the detection window aims a request at a corpse. Found each other? The next question is what they say. Checkout needs a user’s address from the Users service. Two ways to ask. REST, the style that names resources in URLs and uses HTTP verbs for actions, says: point at the thing, GET /users/123, and let the verb (GET, POST, DELETE) say what you want done with it. RPC, the style that calls a named function on another machine, says: call the action,getUser(123), like phoning a function that happens to live on another machine.
Nouns versus verbs. That single difference ripples into speed, typing, caching, and tooling, so it’s worth feeling before memorizing.
Point at nouns. The menu lists things, not chores.
Call verbs. The menu lists chores, done for you.
| You care about… | Pick this | Because |
|---|---|---|
| Public API strangers will use | REST | Readable, debuggable in a browser, universal |
| Raw speed between your own services | gRPC | Binary + HTTP/2 multiplexing, 3–10× smaller payloads |
| Catching contract bugs early | gRPC | Shared schema files generate both sides’ code |
| Caching responses for free | REST | GETs cache at every layer; RPC calls don’t |
| Live streams both directions | gRPC | Streaming is built in, not bolted on |
| Simplest thing that works | REST | curl, browsers, and juniors all speak it day one |
gRPC, the dominant RPC framework built on HTTP/2 with binary encoding, stacks three ideas: a schema language that both sides compile from, binary encoding that stays tiny, and HTTP/2, the multiplexed successor to HTTP/1.1 that carries many streams on one connection, underneath for multiplexed streams. Here’s the whole contract for a user service, this file is the API:
// user.proto
syntax = "proto3";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
}
message GetUserRequest {
int32 id = 1;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}Change the .proto, regenerate, both sides update or compilation fails. Drift becomes impossible instead of inevitable.
That stream User is a live feed, not polling. One connection, endless updates.
No JSON brackets on the wire. Smaller packets, faster parsing, miserable to read by hand, that’s what tools are for.
Generate clients in Go, Python, Java, and TypeScript from the same file. The schema is the shared contract every language compiles from.
Before adopting schemas, teams run JSON REST between their own services at thousands of calls per second, because every engineer already speaks it. It fails on arithmetic. A 200-byte protobuf payload ships as 600–800 bytes of JSON, parsing runs several times slower, and HTTP/1.1 per-connection setup multiplies sockets and handshake churn exactly where calls are densest. The p99 between services climbs while payloads carry field names nobody reads. Keep REST where strangers, browsers, and caches live, and pay the schema cost where both ends are owned and microseconds compound.
The performance gap is measurable, not tribal. A typical JSON REST response carries field names on every message, a 200-byte protobuf payload, protobuf being the binary encoding gRPC uses, often ships as 600–800 bytes of JSON, roughly 3–4× larger, and JSON parsing runs several times slower than protobuf unmarshalling. Over HTTP/2 multiplexing, gRPC reuses one connection for parallel streams where REST over HTTP/1.1 pays per-connection setup; at thousands of internal calls per second, that is fewer sockets, less handshake churn, and visibly lower p99, the slowest 1% of calls, between services you own.
The knobs that matter are deadlines, not encodings. The guidance the industry copied from large RPC operators: default per-call deadlines of a few hundred milliseconds to a few seconds depending on the call, propagated down the chain so a 1-second gateway budget becomes 300ms for service A and 80ms for service B , never a fresh full timeout per hop. Start with connect timeouts near 100ms, response deadlines at 2× the call's p99, and a total budget that fails fast instead of queueing work nobody will wait for.
The split most teams land on is REST at the edges where strangers, browsers, and caches live, and typed RPC inside where both ends are owned and small per-call savings compound across thousands of internal calls per second. Schemas buy compile-time safety and speed at the cost of versioning discipline; REST buys universality and free caching at the cost of payload size and silent drift. And one debuggability cost belongs in the decision: binary protobuf over HTTP/2 is opaque to curl and browser devtools, so expose a REST or browser-speaking RPC gateway at the edge for the frontend developer with curl, or accept that only generated clients and special-purpose tools can speak to the service. Debuggability is a real architectural property, internal speed that nobody outside the team can inspect is a support burden disguised as performance.
Contracts decide what services say to each other, with per-call deadlines of a few hundred milliseconds propagated down the chain, connect timeouts near 100ms, and response deadlines near twice the call's p99, the slowest 1% of calls. But deadlines only decide when to stop waiting. They say nothing about what already happened: the network times out at 5 seconds, the payment provider actually processed the charge, and the next decision, retry or walk away, decides whether one failure stays small or charges the user twice.