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.
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.
| Step | REST (HTTP/JSON) | gRPC (HTTP/2 + protobuf) | JSON-RPC (one endpoint) |
|---|---|---|---|
| 1. Read | GET /articles/42 → 200 {"id":42,"title":"Old","body":"…"} | ArticleSvc.GetArticle{id:42} → Article msg | POST /rpc {"method":"getArticle","params":{"id":42},"id":1} |
| 2. Edit | PATCH /articles/42 {"title":"New"} → 200 | ArticleSvc.UpdateArticle{id:42,title:"New"} | {"method":"updateArticle","params":{"id":42,"title":"New"},"id":2} |
| 3. Retry of step 2 | Use 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:
- Safe (
GET,HEAD): no server state change → freely cacheable and prefetchable. - Idempotent (
GET,PUT,DELETE,HEAD): N identical calls = 1 effect → a client or proxy can safely retry after a timeout. - Neither (
POST): each call may create a new effect → retrying can duplicate.PATCHis not guaranteed idempotent either (aPATCHthat appends, say, differs each time).
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:
- Unary — one request, one response (the ordinary RPC).
- 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. - Client streaming — a stream of requests, one response. e.g. uploading telemetry chunks, then a single ack.
- 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:
| REST | gRPC | |
|---|---|---|
| Backend down | GET /articles/42 → 503 Service Unavailable + Retry-After: 2 — visible to every intermediary on the path | :status 200 … grpc-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
- Retrying a non-idempotent REST
POST. A gateway times out, the client retries, and you get two orders. Fix with idempotency keys or aPUT-with-UUID model — never assume retries are safe. - Chatty REST on mobile. Rendering one screen fires 8 sequential GETs; each round trip costs latency. This is the over-fetching/under-fetching tax that pushes teams to GraphQL or a purpose-built RPC method.
- gRPC in the browser. Browsers cannot speak raw gRPC (no access to HTTP/2 trailers/frames), so you need a
grpc-webproxy (Envoy). Forgetting this is a classic "works in staging, dies in the browser" surprise. - Breaking a protobuf/RPC contract. Reusing a field tag number or changing a type forces client and server to redeploy in lockstep. Always add new fields with new tag numbers; never renumber.
- Opaque intermediaries. A binary gRPC frame is unreadable to your existing HTTP caches, WAFs, and log tooling — you lose the free ecosystem REST enjoys.
- The "RPC is stateful" myth. gRPC and JSON-RPC are stateless per call, exactly like REST; statefulness is a design choice you add (sessions), not an inherent RPC property. Choose between them on coupling and tooling, not on an imagined scalability penalty.
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.
| Dimension | REST | gRPC (RPC) | GraphQL |
|---|---|---|---|
| Best client | Public / third-party / browser | Internal service-to-service | Many client shapes (mobile + web) |
| Contract | Loose (OpenAPI optional) | Strict (.proto, codegen) | Strict (schema/SDL) |
| Payload | JSON (verbose, human-readable) | Protobuf binary (compact, fast) | JSON, client-selected fields |
| HTTP caching | Native (GET + CDN/proxy) | None on payload | Hard (single POST) |
| Streaming | No (need SSE/WebSocket) | First-class, 4 modes | Subscriptions (bolt-on) |
| Coupling | Loose; evolve independently | Tight; codegen keeps client/server in step | Medium; 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
- The core split is nouns + fixed verbs (REST) vs named procedures + argument structs (RPC); caching, coupling, and tooling all follow from that.
- REST inherits idempotency and cacheability from HTTP verbs for free; RPC calls have no verb, so you engineer retry-safety yourself.
- gRPC's real differentiator is binary protobuf + HTTP/2 streaming (unary, server-, client-, bidirectional), ideal internally — but it needs a proxy for browsers and is opaque to HTTP intermediaries.
- Neither is inherently stateful, and "tight coupling" is a cost of RPC, not a benefit; GraphQL is the third lever when varied clients need custom data shapes.
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.
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.
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.
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.
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.