RPC and gRPC
Article.
What is RPC, what does gRPC add to it, and when should you not use it? RPC (remote procedure call) is the model of calling a function in another process as if it were local: a generated stub turns the call into a message, sends it, and turns the reply back into a return value.
gRPC is one implementation of that model (Google, open-sourced 2015, 1.0 in 2016), with Protobuf contracts, HTTP/2 transport, streaming and deadlines.
The senior answer starts with why the “as if local” illusion leaks, because every design rule for RPC follows from it.
Current: grpcio 1.84.0 (2026-09-14) and protobuf 7.36 on PyPI. The
comparison with REST, GraphQL and SOAP is in
API Protocols Comparison — REST, GraphQL, gRPC, SOAP; the
gRPC depth is in the numbered notes listed under Related.
Why the local-call illusion leaks
A Note on Distributed Computing (Waldo et al., Sun, 1994) made the point that still holds: a remote call differs from a local one in ways no interface can hide.
- Latency: a local call takes nanoseconds, a remote one a millisecond or more. A loop of “harmless” calls is a loop of network round trips.
- Partial failure: a local call returns or the process dies. A remote call can time out with no way of knowing whether it ran. Was the order created? The caller cannot tell from the failure.
- No shared memory: arguments are copied, not referenced; mutating an object on the server changes nothing on the client.
- Independent failure and versioning: the server deploys on its own schedule, so caller and callee run different code at the same time.
So every RPC needs what a local call never does: a deadline, a decision about retries, which requires the operation to be idempotent (or deduplicated by a key), and compatible schema evolution. A framework that makes remote calls look local makes these easy to forget, which is the classic criticism of RPC. gRPC’s answer is to make deadlines, status codes and streaming explicit in the API.
The RPC family
| Protocol | Encoding and transport | Where you meet it |
|---|---|---|
| XML-RPC, SOAP | XML over HTTP | legacy enterprise, banking, government |
| JSON-RPC 2.0 | JSON, any transport | Ethereum nodes, MCP (Model Context Protocol) |
| Thrift | binary, own transports | Facebook-era services, Cassandra’s old API |
| gRPC | Protobuf over HTTP/2 | service-to-service, Kubernetes ecosystem |
| Connect | Protobuf or JSON, HTTP/1.1 or 2 | gRPC-compatible, browser-friendly |
JSON-RPC 2.0 is worth knowing as the simplest member: a request is
{"jsonrpc": "2.0", "method": "tools/call", "params": {...}, "id": 7}, a
response carries the same id with a result or an error, and a request
with no id is a notification that gets no reply. It is the envelope MCP
uses between AI clients and tool servers
(Agent protocols: MCP, A2A and the interop layer).
What gRPC adds
A contract that generates code
syntax = "proto3";
package shop.v1;
service OrderService {
rpc GetOrder (GetOrderRequest) returns (Order);
rpc WatchOrders (WatchRequest) returns (stream OrderEvent); // server streaming
}
message GetOrderRequest { string id = 1; }
message Order {
string id = 1; int64 customer_id = 2; string currency = 3;
repeated Item items = 4; string status = 6; // field 5 was removed: never reuse it
reserved 5;
}python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. order.proto generates messages and stubs. Fields are identified on the wire
by number, not name: renaming a field is safe, changing or reusing a
number is not, and removed numbers belong in reserved. Schema rules are in
Protocol Buffers (Protobuf).
HTTP/2 underneath
Each call is one HTTP/2 stream: a POST to /shop.v1.OrderService/GetOrder
with content-type: application/grpc, a body of length-prefixed messages
(a 1-byte compression flag and a 4-byte length before each), and the result
status in trailers (grpc-status, grpc-message) after the body. Many
calls share one TCP connection.
Two consequences come straight from that:
- Browsers cannot speak it: JavaScript has no access to HTTP/2 trailers or framing, so browsers need gRPC-Web (through a proxy such as Envoy) or the Connect protocol.
- Streaming is just a long stream, not HTTP/2 server push (which gRPC never used): messages flow as DATA frames until one side ends the stream. The four method types (unary, server, client and bidirectional streaming) are in gRPC Service Types — Unary and Streaming.
Deadlines, status codes and metadata
import grpc
from shop.v1 import order_pb2, order_pb2_grpc
channel = grpc.insecure_channel("orders:50051")
stub = order_pb2_grpc.OrderServiceStub(channel)
try:
order = stub.GetOrder(order_pb2.GetOrderRequest(id="ord_8f3a2c"),
timeout=0.3, # the deadline
metadata=[("x-request-id", req_id)])
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
... # did it run? unknown
elif e.code() == grpc.StatusCode.NOT_FOUND:
...The server side checks the remaining time before starting work:
class OrderService(order_pb2_grpc.OrderServiceServicer):
def GetOrder(self, request, context):
if context.time_remaining() < 0.05: # not worth starting
context.abort(grpc.StatusCode.DEADLINE_EXCEEDED, "no time left")
order = repo.get(request.id)
if order is None:
context.abort(grpc.StatusCode.NOT_FOUND, f"order {request.id}")
return orderWhat the code relies on
- The deadline travels with the call (the
grpc-timeoutheader), so a server can check how much time remains and pass the remainder on to its own calls. Without one, a Python client waits indefinitely. - gRPC has its own 17 status codes (
OKtoUNAUTHENTICATED), not HTTP statuses;UNAVAILABLEmeans “safe to retry”,DEADLINE_EXCEEDEDmeans “unknown outcome”. Details in gRPC Error Handling. - Retries are configured, not hand-written: a service config with a
retryPolicy(max attempts, backoff,retryableStatusCodes: ["UNAVAILABLE"]), passed as the channel optiongrpc.service_config. - Metadata carries auth tokens and request ids, like headers; interceptors add them everywhere (gRPC Interceptors, Metadata, and Auth).
Is it faster? Measured
A 20-item order message, Python 3.14.5, protobuf 7.36.2 (C-backed):
size protobuf 395 B json 1150 B ratio 2.9x
encode protobuf 1.2 us json 15.4 us
decode protobuf 1.8 us json 14.8 us
gzip size protobuf 187 B json 277 B
orjson encode 2.0 us decode 5.6 us- Against the standard library’s
json, Protobuf is about 3x smaller and 10x faster to encode and decode, which is where “gRPC is 5-10x faster” claims come from. - Against orjson, the gap is under 2x to encode and about 3x to decode; with gzip, the size gap shrinks to 1.5x.
- Either way it is microseconds per message against milliseconds of network and database time. Serialisation speed decides little for typical calls; it matters for high-volume streams and large messages.
The real reasons to choose gRPC are the contract and codegen across languages, streaming, deadlines and status codes as first-class concepts, and multiplexing many calls over one connection.
gRPC or REST
| gRPC | REST with JSON | |
|---|---|---|
| Contract | .proto, enforced on the wire |
OpenAPI, by convention |
| Browser | needs gRPC-Web or Connect | native |
| Streaming | four built-in kinds | SSE or WebSocket alongside |
| Caching | none at the HTTP layer | HTTP caching, CDNs |
| Debugging | binary; needs grpcurl |
curl and a browser |
| Errors | 17 gRPC codes in trailers | HTTP status and body |
Use gRPC between services you own, especially polyglot ones and anything streaming. Use REST at the edge: browsers, partners, public APIs, anything that benefits from HTTP caching. “REST outside, gRPC inside” is a common, sound split.
Where it breaks in production
- All traffic on one pod. A Kubernetes
ClusterIPService balances connections, not requests, and gRPC keeps one long-lived HTTP/2 connection; every call from a client goes to the same backend. Use a headless Service with client-sideround_robin, or an L7 proxy or mesh (gRPC Load Balancing and gRPC-Web). - Hung calls with no deadline: a slow dependency holds threads indefinitely. Every call gets a deadline, propagated downstream.
- Retries on non-idempotent calls: a retried
DEADLINE_EXCEEDEDcreates the order twice. Retry onlyUNAVAILABLEby default, and use request ids for writes. - A reused field number: an old client decodes a new field as the old
one, silently wrong.
reservedremoved numbers, and run a breaking-change check (buf breaking) in CI. - Messages over 4 MB: the default maximum receive size rejects them
with
RESOURCE_EXHAUSTED; stream large payloads instead of raising the limit. - Proxies that break HTTP/2 or trailers: an HTTP/1.1 hop, a load balancer without gRPC support, or an idle timeout shorter than the stream; keepalive pings and gRPC-aware proxies fix them.
The trade not taken
REST with JSON and OpenAPI everywhere. One protocol for browsers,
partners and services; curl-debuggable; HTTP caching; no codegen step.
It costs typed contracts enforced on the wire, built-in streaming and
deadline propagation, which teams then rebuild by convention. Connect
(from Buf) is the middle ground: the same .proto services, callable as
gRPC, gRPC-Web or plain HTTP with JSON, from browsers without a proxy.
Related
Interview angle 4
- “What is RPC, and what is wrong with it?” - calling a remote function
through a generated stub as if it were local. The illusion leaks: latency,
partial failure, no shared memory, independent deploys.
- Follow-up: a call timed out. Did it run? Unknown. That is why remote writes need idempotency keys, and why gRPC makes deadlines and status codes explicit.
- “What does gRPC add over plain RPC?” - Protobuf contracts with
codegen, HTTP/2 multiplexing, four streaming modes, deadlines that
propagate, its own status codes and configurable retries.
- Follow-up: why can’t a browser call it directly? Status comes in HTTP/2 trailers and JavaScript cannot read them; gRPC-Web or Connect bridges it.
- “Is gRPC really 5-10x faster than REST?” - Protobuf is about 3x
smaller and 10x faster than stdlib
jsonper message, but under 2-3x against orjson, 1.5x smaller after gzip, and microseconds either way. The network dominates; choose it for contracts and streaming. - “You deployed gRPC on Kubernetes and one pod takes all the load.” - the Service balances connections, and gRPC holds one connection open. Client-side balancing over a headless Service, or an L7 proxy or mesh.