Cover illustration for “API vs RPC Architectural Trade-Offs”
APILong read

API vs RPC Architectural Trade-Offs

REST's simplicity hides costly trade-offs against gRPC's streaming and type safety.

Senior Writer · · 9 min read

Every engineer building an API eventually has to answer one question: is the world made of nouns or verbs? REST says nouns, resources with URLs, manipulated by a handful of standard verbs. RPC says verbs, procedures you call directly, like getEmployee(123) instead of GET /employees/123. That single choice, made early and often by accident, decides how a team thinks about domain modeling, caching, type safety, and streaming for the rest of the project's life.

Most engineers meet this decision dressed up as a tooling question: "should we use gRPC or REST?" That framing skips the actual fork in the road. The protocol is just the mechanism. The mental model quietly shapes every API design conversation that follows.

How each protocol encodes its mental model in concrete mechanics

REST keeps things simple on paper: one URL per resource, and the HTTP verb does the talking. GET reads, POST creates, PUT or PATCH updates, DELETE removes. The server decides what shape the response takes, and there's no session state to keep track of between calls. It's the API equivalent of a well-labeled filing cabinet: everything has a drawer, and the drawer's label tells you what you're allowed to do with what's inside.

gRPC flips the metaphor. It's schema-first, built on Protocol Buffers, and the service definitions read like you're calling a method on an object sitting right next to you, even when that object lives three data centers away. The wire format is binary, it rides on HTTP/2, and code generation spits out typed client stubs in nearly any language you'd want. No guessing about field names. No runtime surprises about what a response looks like, because the shape was decided at compile time.

GraphQL takes a third path: single endpoint, query language layered on top of HTTP, and the client gets to decide exactly which fields it wants back. The server resolves each field through a type system, and none of it works without a schema definition language (SDL) laying out what's queryable. It's a menu where you build your own plate instead of ordering a fixed combo.

tRPC skips the schema step. No SDL, no code generation pipeline, just the TypeScript compiler inferring types straight from the server's router definitions. Changing a field name on the server breaks the client-side build immediately, at compile time, not in production at 2 a.m. It only works when both sides share a TypeScript codebase, which in practice means a monorepo. It's less a protocol and more a very well-fitted glove for a specific setup.

What performance data shows, and what it obscures

The caveats matter more than the headline, so the numbers come first and the caveats follow.

At 10,000 requests per second with a 1KB payload on standardized AWS c6i.2xlarge instances, gRPC on Go with Protobuf posts a P50 latency of 0.84 milliseconds. REST on Rust with Axum is 1.12 milliseconds. REST on Node.js with Fastify jumps to 3.45 milliseconds, and tRPC on the same Node.js stack lands close behind at 3.82. GraphQL on Node.js with Apollo trails the pack at 8.90 milliseconds, but swap in Rust with async-graphql and that number drops to 2.15.

See the pattern yet? The runtime is doing as much work as the protocol.

Throughput tells the same story from a different angle. REST on Rust with Axum leads at 22,100 requests per second per core. gRPC on Go follows at 18,450. GraphQL on Rust manages 11,200, while REST on Node.js drops to 6,200 and tRPC on Node.js sits close by at 5,850. GraphQL on Node.js with Apollo comes in last at 1,850.

That throughput gap has a dollar sign attached. Estimated cost per million operations: gRPC on Go at $0.008, REST on Rust at $0.005, REST on Node.js at $0.038, tRPC on Node.js at $0.042, GraphQL on Rust at $0.015, and GraphQL on Node.js with Apollo at $0.185. That's a huge spread between the cheapest and most expensive option, and the protocol alone doesn't explain it. The runtime choice does at least as much heavy lifting as the protocol choice, and teams that benchmark protocols while holding the runtime constant are only getting half the picture.

Payload size flips the script in GraphQL's favor. In a benchmark from pockit.tools, GraphQL fetched the same data in 834 bytes against REST's 1,247 bytes. Query parsing on the server costs measurable overhead. But when a client only needs a handful of fields out of a much larger object, GraphQL's ability to ask for what it wants saves real bandwidth, especially on mobile networks where every kilobyte has a cost attached to it.

Diagram: Protocol Performance: Latency and Cost per Million Operations. Visualizes: Show how six protocol/runtime combinations compare on two dimensions: P50 latency (ms) at 10,000 req/s and estimated cost per million operations.

Caching, streaming, and the four capabilities REST can't match natively

Statelessness is REST's quiet superpower. Because nothing about the server needs to remember the last request, horizontal scaling is close to trivial, and standard HTTP cache headers work right out of the box with any content delivery service or browser that's ever existed. Binary protocols lose this almost entirely, and GraphQL, funneling everything through a single POST endpoint, makes caching awkward enough that most teams bolt on custom solutions just to get back what REST gave them for free.

Streaming is where the gap really opens up, and it's structural, not a matter of tuning. REST supports exactly one pattern natively: unary, one request, one response, and that's the end of the conversation. gRPC, by contrast, supports four distinct communication patterns.

Unary works the same as REST. Server streaming lets the server send back a sequence of messages for a single client request, useful for real-time feeds, log tailing, or progress updates on a long job. Client streaming reverses it: the client sends a stream, the server replies once, which fits bulk uploads, sensor telemetry, or batch processing. Bidirectional streaming lets both sides talk at once, which is what chat apps, collaborative editors, and live dashboards actually need under the hood.

GraphQL has subscriptions for real-time cases, but the fit is awkward enough that analysis from pockit.tools shows most teams reach for server-sent events instead of GraphQL's own subscription model. tRPC, meanwhile, is JSON riding on top of HTTP, plain and simple, so it inherits REST's ceiling on streaming. No native support, no workaround baked in.

Type safety across the four protocols and its cost

Type safety is a spectrum, and where each protocol sits on it comes with a price tag attached. It's a spectrum, and where each protocol sits on it comes with a price tag attached.

REST's type safety is opt-in. The team writes types by hand, or wires up OpenAPI codegen, and when a field name changes on the server without anyone updating the client, that mismatch appears as a runtime error, not a compile error. Pockit.tools has a phrase for the ergonomic reality here: "trust me bro." That's not a knock; it's just accurate.

GraphQL does better through SDL codegen, generating types from the schema automatically. That adds one more step to the build pipeline, and it's a step that's easy to forget. Skipping a regeneration after a schema change lets the types quietly go stale, a genuine operational risk on any codebase that ships more than once a week.

gRPC bakes type safety into the workflow itself. Types are generated straight from .proto files, codegen is mandatory, and it has to live in the build pipeline or nothing compiles. Protocol Buffers' interface definition language becomes the single source of truth, which is a real strength: every service, in every language, agrees on the same contract. The cost shows up in the learning curve, proto syntax, field numbering rules, and the generation workflow all take time to get comfortable with.

tRPC gets type safety essentially for free, no codegen step at all, because the TypeScript compiler does the enforcement directly. The tradeoff is the one already mentioned: it only works inside a single TypeScript codebase. Powerful, but narrow.

How API versioning strategy differs by protocol, and why most teams underinvest in it

Only 26% of teams implement semantic versioning, even though 60% version their APIs in some form. Read that gap carefully. Most teams that bother to version at all are doing it without a real strategy behind it, which tends to mean the strategy gets invented under pressure, usually right after something breaks.

REST offers three common patterns, and each one trades a convenience for a cost. Path-based versioning (/v1/resource) is the crowd favorite: routing is obvious, documentation is easy, and gateways can route on it without any cleverness. Left unmanaged, though, it produces cloned endpoints and what kreya.app calls "bloated legacy paths that are terrifying to delete." Header-based versioning keeps URIs clean and supports proper content negotiation, but it's invisible to anyone poking around with a browser, and harder to test without dedicated tooling. Host-based versioning (v1.api.example.com) gives full domain separation between versions, at the cost of the highest operational overhead of the three.

GraphQL sidesteps versioning almost entirely by design. New fields and types get added additively, old fields get marked deprecated rather than deleted, and the schema just keeps growing outward. That works fine right up until a field genuinely has to go, at which point deprecation needs active management, not just a tag and a prayer.

gRPC handles this with the most discipline of the four. Fields evolve by adding new fields with new numbers, and removed field numbers get reserved so nobody accidentally reuses them and silently breaks backward compatibility. Full service version bumps get saved for the rare breaking change that can't be handled additively. It's versioning enforced by the schema itself, not by convention or good intentions.

Diagram: gRPC's Four Streaming Patterns vs. REST's One. Visualizes: Illustrate the four communication patterns gRPC supports natively versus REST's single pattern.

The hybrid architecture most production systems already use

The framing that pits one protocol against another mostly falls apart once you look at what's actually running in production. The dominant pattern in modern SaaS isn't one protocol, it's three layers stacked on top of each other, each one solving a different problem for a different audience.

The outer layer is a public REST API, handling integrations, webhooks, and SDKs, the surface facing the outside world. Here, human readability, wide compatibility, and a stable contract are prioritized over shaving off a millisecond. The middle layer is usually tRPC or GraphQL, powering the product's own frontend, a place where the team controls both client and server, type safety pays for itself, and payload flexibility earns its keep. The inner layer, service-to-service backend traffic, tends to run on gRPC or REST, wherever both sides are controlled internally, call volume is high, and schema strictness plus sub-millisecond latency actually move the needle.

Each layer answers to a different consumer with different needs, and that's the real insight: protocol choice is downstream of asking who's calling and what they actually need from the call.

The gateway pattern is the practical mechanism for moving from one protocol style to another without breaking anyone who's already integrated, allowing teams to introduce a new protocol underneath while keeping existing client-facing contracts intact through the transition.

A decision framework for choosing the right protocol at the right boundary

The right question isn't "which protocol is best." It's "what boundary am I building for, and who's on the other side of it." Organize by boundary, not by preference, and the choice mostly makes itself.

For a public API or a third-party integration surface, REST is the sensible default. Broad compatibility, human-readable payloads, no learning curve for whoever's consuming it, and a tooling ecosystem that's had decades to mature. Pairing REST with OpenAPI or Swagger gets most of the benefits of a formal interface definition, codegen, testing, documentation, without forcing a binary protocol onto consumers who never asked for one. Versioning discipline stops being optional at this boundary: explicit versioning for anything that breaks compatibility, additive changes for everything else, and an actual plan for retiring old versions instead of letting them pile up like unread email.

For the product's own frontend, where the team owns both ends of the wire, tRPC or GraphQL earns its complexity. Type safety and payload flexibility pay rent here in a way they don't for outside consumers. And for high-volume service-to-service traffic behind the scenes, gRPC (or REST, where the extra ceremony of Protobuf isn't worth it) fits the job: strict schemas, predictable latency, and no need to explain any of it to somebody outside the building.

None of these choices are permanent, and none of them need to be made in isolation. The gateway pattern proves that a system can run more than one protocol at once and still hold together. What actually breaks systems isn't picking REST over gRPC, or GraphQL over tRPC, it's picking one by habit and never asking which boundary it's actually serving.

Sources

  1. API Architecture Comparison: REST vs. GraphQL vs. tRPC vs. gRPC for Cloud-Native Backends
  2. REST vs. gRPC - Which should You choose for your next API? | Kreya
  3. REST vs GraphQL vs tRPC vs gRPC in 2026: The Definitive Guide to Choosing Your API Layer
  4. GraphQL vs REST vs gRPC: API Architecture Comparison in 2026
  5. digitalapplied.com
Filed underAPI

More in API