RPC and gRPC

Updated 4 interview angles 8 min read source
On this page9
  1. Why the local-call illusion leaks
  2. The RPC family
  3. What gRPC adds
  4. Is it faster? Measured
  5. gRPC or REST
  6. Where it breaks in production
  7. The trade not taken
  8. Related
  9. Interview angle

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

protobuf
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

python
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:

python
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 order

What the code relies on

  • The deadline travels with the call (the grpc-timeout header), 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 (OK to UNAUTHENTICATED), not HTTP statuses; UNAVAILABLE means “safe to retry”, DEADLINE_EXCEEDED means “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 option grpc.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):

text
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 ClusterIP Service 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-side round_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_EXCEEDED creates the order twice. Retry only UNAVAILABLE by default, and use request ids for writes.
  • A reused field number: an old client decodes a new field as the old one, silently wrong. reserved removed 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.

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 json per 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.