gRPC and Modern RPC
gRPC is the dominant framework for service-to-service calls inside modern systems. It bundles three things: Protocol Buffers for a typed binary contract, HTTP/2 as the multiplexed transport, and code generation that turns a schema file into client and server stubs in a dozen languages. You define a service once in a .proto file, generate the code, and call a remote method as if it were a local function with typed arguments and a typed return.
The trade it makes is deliberate. gRPC gives up REST's human-readable JSON and curl-from-anywhere accessibility in exchange for compact binary encoding, native streaming, and a strict schema the compiler enforces on both ends. Inside a datacenter, between services you control, that is usually the right call. At a public edge where browsers and unknown clients live, it is usually the wrong one — browsers cannot even speak raw gRPC.
Protocol Buffers — the IDL and Wire Format
A .proto file is the interface definition: it declares messages (typed structs) and services (collections of RPC methods). Each message field has a name, a type, and — critically — a field number. That number, not the field name, is what goes on the wire; the binary encoding tags each value with its field number, so the encoded form is compact and the names exist only in the schema. A message is a sequence of (field number, type, value) tuples, far smaller than the equivalent JSON.
Field numbers are also the contract for schema evolution. You can add a new field with a new number and old clients ignore it; you can stop using a field. What you must never do is reuse a retired field number for a different field — an old peer will decode the new value into the old field's slot and silently corrupt data. The rule is one line: never change or reuse a field number once it has shipped. Mark removed numbers reserved so the compiler stops anyone from reusing them.
// a .proto contract — field numbers are the wire identity message User { string name = 1; // field 1, never renumber string email = 2; reserved 3; // retired field, blocked from reuse int32 age = 4; // added later; old clients ignore it } service Users { rpc GetUser(GetUserRequest) returns (User); rpc WatchUsers(WatchRequest) returns (stream User); // server stream }
The Four Call Types
gRPC supports four call shapes, and you pick per method in the .proto. Unary is the familiar one request, one response — a normal function call. Server streaming sends one request and gets back a stream of responses: a subscription, a paginated feed, a result set arriving over time. Client streaming is the reverse — the client sends a stream and the server returns one response, good for uploads or aggregating many data points into a summary.
Bidirectional streaming opens both directions at once: client and server each send a stream independently over the same call, which maps onto chat, live coordination, or a long interactive session. All four ride the same HTTP/2 connection, which is exactly why gRPC needs HTTP/2 and not HTTP/1.1 — streaming and multiplexing are not bolted on, they are the transport's native model.
Why gRPC Requires HTTP/2
gRPC is built on HTTP/2's capabilities, not just layered over it. Multiplexing lets many concurrent calls share one connection without head-of-line blocking at the HTTP layer, so a service mesh holds a handful of long-lived connections instead of one per call. HTTP/2's bidirectional streams are what make client, server, and bidi streaming possible at all — HTTP/1.1's strict request/response ordering could not express them.
gRPC also leans on HTTP/2 trailers — headers sent after the response body — to carry the final status (grpc-status and grpc-message). A streaming call cannot know its success or failure until the stream ends, so the status has to come last, and only HTTP/2 trailers allow that. HTTP/2 flow control, per-stream and per-connection, keeps one large stream from starving the others on the shared connection.
Edge versus Internal
Browsers cannot speak raw gRPC. JavaScript has no access to HTTP/2 trailers or the raw frame control gRPC needs, so a browser literally cannot make a standard gRPC call. The workaround is gRPC-Web, a slightly different protocol that a proxy (Envoy, or a gateway) translates to and from real gRPC at the edge. Public APIs often sit behind a gRPC gateway that also exposes a REST/JSON facade, so external clients get familiar HTTP while internal services keep the typed binary contract.
Load balancing is the other edge trap. gRPC's long-lived HTTP/2 connections defeat a naive layer-4 load balancer — it pins all the multiplexed calls to whichever backend the connection first hit, so one backend gets hammered while others idle. You need a gRPC-aware (layer-7) balancer that load-balances individual calls across backends, or client-side load balancing. The rule for the edge is simple: terminate or translate gRPC at a proxy that understands it, and never assume a generic L4 balancer distributes gRPC traffic.
gRPC gives a typed binary contract, native streaming in all four call shapes, and generated stubs in many languages — compact and fast, with the schema enforced by the compiler. The cost is that it is unreadable without the .proto, needs HTTP/2 end to end, and cannot be called from a browser without gRPC-Web. Choose it for internal service-to-service calls where performance and a strict contract matter.
REST over JSON is human-readable, callable from curl and any browser, and works with every cache, proxy, and gateway out of the box. It is more verbose, has no native streaming, and enforces no schema unless you add one. Choose it at the public edge and anywhere broad accessibility and ubiquitous tooling outweigh raw efficiency.
- Exposing raw gRPC directly to browsers. They cannot speak it — no access to HTTP/2 trailers — so calls fail outright; you need gRPC-Web through a translating proxy or a REST gateway at the edge.
- Reusing a retired field number for a new field in a
.proto. An old peer decodes the new value into the old field's slot and silently corrupts data with no error — mark removed numbersreservedso the compiler blocks it. - Putting gRPC behind a layer-4 load balancer. It pins every multiplexed call on a connection to one backend, so that backend is overloaded while others sit idle — you need a gRPC-aware L7 or client-side balancer.
- Setting no deadline on a gRPC call. Without a deadline a hung server holds the call (and resources) indefinitely, and failures cascade as callers pile up waiting — every call should carry an explicit timeout.
- Changing a field's type while keeping its number, expecting compatibility. The number matches but the decoder misreads the bytes, producing garbage values that pass type checks and surface as subtle data bugs downstream.
- Treat field numbers as permanent: never renumber or reuse them, and mark removed fields
reservedso the compiler enforces backward compatibility for every shipped client. - Set an explicit deadline on every gRPC call so a hung backend cannot hold resources indefinitely and failures fail fast instead of cascading.
- Front public-facing gRPC with a gateway that offers gRPC-Web or a REST/JSON facade, so browsers and external clients can call services that internally keep the typed contract.
- Use a gRPC-aware layer-7 load balancer (or client-side balancing) so individual calls spread across backends instead of pinning to one via the shared HTTP/2 connection.
- Keep gRPC for internal service-to-service traffic where the schema and performance pay off, and expose REST at the edge where ubiquitous tooling and browser access matter more.
Knowledge Check
Why does reusing a retired Protocol Buffers field number for a new field corrupt data silently?
- The number is the wire identity, so an old peer decodes the new value into the old field
- Two fields share a name on the wire, so the decoder cannot tell them apart
- Field numbers must stay sequential, and a gap breaks the binary layout
- The full schema definition travels embedded with each message, and the mismatched copy then fails strict validation
Why does gRPC require HTTP/2 rather than working over HTTP/1.1?
- It needs HTTP/2's bidirectional streams and trailers, which carry the final call status
- HTTP/1.1 cannot transmit the binary Protocol Buffers payloads gRPC sends
- Only HTTP/2 encrypts the connection, which gRPC's contract requires
- HTTP/2's HPACK header compression is strictly mandatory for encoding the generated .proto schema on the wire
A team puts gRPC services behind a plain layer-4 load balancer and one backend gets overloaded. Why?
- An L4 balancer pins the whole long-lived connection, so all its multiplexed calls hit one backend
- gRPC opens a fresh connection per call, overwhelming whichever backend answers first
- Protocol Buffers payloads are so large they saturate a single backend's bandwidth
- The balancer inherently requires sticky affinity sessions for all gRPC traffic and routes every single client to one node
You got correct