CMD Guide
HomeSystem DesignSystem Design Trade-offs

REST vs RPC

REST and RPC differ in one mechanical choice: what the wire names. REST names nouns — server state is a set of resources at URLs, and a tiny fixed verb set (GET, POST, PUT, PATCH, DELETE) acts on them; RPC names verbs — the client calls a function by name and ships its arguments, and generated client stubs make a network call look like a local method invocation. Everything else (payload format, caching behaviour, coupling) falls out of that one decision.

The two mechanisms side by side

REST bends every operation into verb + resource: to change an article's title you PATCH /articles/42. The set of verbs is fixed and their meaning is defined by HTTP, so any generic HTTP tool — a browser, a CDN, a proxy, curl — already understands the call without knowing your app. RPC does the opposite: the operation is the name. You call UpdateArticle(id=42, title="…"); the transport is just a pipe carrying a serialized argument struct to a matching function on the server. gRPC serializes that struct as binary Protocol Buffers over a single multiplexed HTTP/2 connection; JSON-RPC and XML-RPC send a text envelope, usually over one POST endpoint.

diagram
diagram

Worked trace: read then edit article 42

A blog editor opens article 42, changes the title, and (on a flaky network) the update request is retried once. Watch the same three logical steps in each style.

StepREST (HTTP/JSON)gRPC (HTTP/2 + protobuf)JSON-RPC (one endpoint)
1. ReadGET /articles/42200 {"id":42,"title":"Old","body":"…"}ArticleSvc.GetArticle{id:42}Article msgPOST /rpc {"method":"getArticle","params":{"id":42},"id":1}
2. EditPATCH /articles/42 {"title":"New"}200ArticleSvc.UpdateArticle{id:42,title:"New"}{"method":"updateArticle","params":{"id":42,"title":"New"},"id":2}
3. Retry of step 2Use PUT /articles/42 (full resource) → idempotent: two identical writes leave one final state. A retried non-idempotent POST /articles would create two rows.Server must dedupe by a request id or make the method idempotent itself — HTTP gives no built-in guarantee here.Same: JSON-RPC has no verb semantics, so idempotency is entirely the method's responsibility.
Wire size (step 1)~90 bytes JSON + headers~12 bytes protobuf + shared HPACK headers~70 bytes JSON + headers

The key observation from step 3: REST gets retry-safety for free from the verb (GET/PUT/DELETE are idempotent by contract); RPC calls have no verb, so you engineer idempotency by hand.

HTTP verbs and idempotency — the part REST leans on

REST's payoff comes from HTTP method semantics, which every intermediary honours:

This is why a mature REST API models "create payment" carefully: either use a client-supplied idempotency key on the POST, or model it as PUT /payments/{uuid} so a retry is harmless. RPC frameworks give you none of this by default — the framework just re-sends the call.

gRPC streaming — the capability REST lacks

Because gRPC rides HTTP/2, a single method can hold a long-lived, multiplexed stream instead of one request/response. There are four call shapes:

  1. Unary — one request, one response (the ordinary RPC).
  2. Server streaming — one request, a stream of responses. e.g. SubscribePrices(symbol:"AAPL") pushes a tick message every time the price moves, over one connection.
  3. Client streaming — a stream of requests, one response. e.g. uploading telemetry chunks, then a single ack.
  4. Bidirectional — both sides stream independently over the same connection (chat, live collaboration).

Mechanically: HTTP/2 frames let many streams share one TCP connection with per-stream flow control, and protobuf keeps each message tiny. Plain REST over HTTP/1.1 has no first-class server-push equivalent — you reach for polling, SSE, or WebSockets (covered in the streaming lessons). This, not "speed" alone, is often the deciding reason to pick gRPC internally.

Error models — where the same failure looks completely different

REST errors ride the HTTP status code itself, so every cache, proxy, SDK, and dashboard already understands them: a 429 triggers generic client backoff logic, and a 503 with Retry-After: 2 is honoured by intermediaries that have never heard of your app. gRPC does not reuse HTTP status codes for call outcomes. It has its own status-code set, and — per the gRPC-over-HTTP/2 protocol — a failed call still arrives with HTTP :status 200; the real outcome travels in the grpc-status trailer at the end of the stream. Watch one failed call twice:

RESTgRPC
Backend downGET /articles/42503 Service Unavailable + Retry-After: 2 — visible to every intermediary on the path:status 200grpc-status: 14 (UNAVAILABLE) in the trailers — invisible to anything that only reads the HTTP status line

The status codes map, but only if your tooling is gRPC-aware: UNAVAILABLE is the moral equivalent of a 503 (retryable, back off), DEADLINE_EXCEEDED of a 504 (the time budget ran out), RESOURCE_EXHAUSTED of a 429 (quota, shed load). This is the answer to a real production mystery: "why did our HTTP 5xx alarm stay green while the gRPC service was down?" — the L7 dashboard and the WAF were keying on HTTP status, saw a healthy stream of 200s, and the failures were all in trailers they never parsed.

The second difference is deadlines. A gRPC client attaches a deadline to the call and it propagates hop-to-hop: each callee sees the caller's remaining budget and can give up early instead of doing doomed work. REST has no built-in equivalent — each hop invents its own timeout independently, which is exactly how timeout inversion sneaks in: a downstream service configured with a longer timeout than its upstream keeps grinding on a request whose caller hung up seconds ago.

The modern third option: GraphQL

GraphQL sits between the two. Like RPC it uses one endpoint (POST /graphql), but instead of naming a procedure the client sends a query describing exactly the fields it wants, and the server returns precisely that shape:

query { article(id: 42) { title author { name } } }

It directly attacks REST's over-/under-fetching: a mobile screen that needs an article title plus author name fetches both in one round trip, no extra fields, no second call. The costs are real, though: HTTP caching no longer works out of the box (everything is a POST to one URL), a naive resolver hits the N+1 query problem, and a malicious deeply-nested query can be a denial-of-service vector unless you add query-cost limits. Treat GraphQL as "client-shaped REST" for read-heavy, many-client products — not a universal replacement.

Pitfalls a working engineer hits

When to use which — and what it costs

Decide on who the client is and how much you value HTTP's free ecosystem versus a tight typed contract.

DimensionRESTgRPC (RPC)GraphQL
Best clientPublic / third-party / browserInternal service-to-serviceMany client shapes (mobile + web)
ContractLoose (OpenAPI optional)Strict (.proto, codegen)Strict (schema/SDL)
PayloadJSON (verbose, human-readable)Protobuf binary (compact, fast)JSON, client-selected fields
HTTP cachingNative (GET + CDN/proxy)None on payloadHard (single POST)
StreamingNo (need SSE/WebSocket)First-class, 4 modesSubscriptions (bolt-on)
CouplingLoose; evolve independentlyTight; codegen keeps client/server in stepMedium; schema is the contract

Choose REST when the API is public, browser-facing, or benefits from caches and proxies, and loose evolution matters more than raw speed. Prefer gRPC when it is internal, polyglot, latency/throughput-sensitive, or needs streaming — you accept tighter coupling and a grpc-web proxy for browsers in exchange for a compact binary contract. Prefer GraphQL when diverse clients need different slices of nested data and endpoint proliferation or over-fetching is your actual pain — you accept losing HTTP caching and taking on query-cost defence.

Takeaways


Re-authored and deepened for this guide. Sources: Roy Fielding, "Architectural Styles and the Design of Network-based Software Architectures" (REST dissertation, 2000); RFC 9110 (HTTP Semantics — safe/idempotent method definitions); the official gRPC docs (Core concepts, HTTP/2 & streaming) and Protocol Buffers language guide; JSON-RPC 2.0 specification; the GraphQL specification (graphql.org). Cross-checked against common API-design guidance in Newman, Building Microservices, 2nd ed.

🤖 Don't fully get this? Learn it with Claude

Stuck on REST vs RPC? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **REST vs RPC** (System Design) and want to truly understand it. Explain REST vs RPC from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **REST vs RPC** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **REST vs RPC** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **REST vs RPC** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes