gRPC vs tRPC vs every other RPC

This is the companion piece to gRPC vs REST vs GraphQL vs everything else — battle, which compares RPC against the things that aren’t RPC. This one goes inside the family — and the family is much older and much stranger than the two names in the title suggest.

Let’s deal with the title question first, because it has a short answer: gRPC and tRPC are not alternatives. They share four letters and almost nothing else. One is a cross-language wire protocol with a binary encoding and a schema compiler; the other is a TypeScript type-inference trick that produces no protocol at all. If you’re genuinely torn between them, you’re not choosing a technology — you’re deciding whether your client and server are one deployable or two, and that’s an org chart question wearing a framework costume.

The interesting part is why the question keeps getting asked, and what forty years of the rest of the family — CORBA, RMI, XML-RPC, Thrift, Twirp, JSON-RPC, Cap’n Proto — already learned the hard way.

gRPC vs tRPC, head to head

gRPCtRPC
What it actually isa wire protocol + schema compilera TypeScript type-inference library
Contract lives ina .proto file, versioned as an artifactyour server’s source code
Codegenyes — stubs per languagenone — the client imports the server’s types
Languages13 with official guidesTypeScript. That’s the list.
Wire formatProtobuf binaryJSON (or superjson)
TransportHTTP/2 (HTTP/3 only in grpc-dotnet)any HTTP; it doesn’t define a protocol
Streamingunary + 3 streaming modes, first-classsubscriptions over WS/SSE
Browserneeds grpc-web + a proxynative — it is a browser client
Type safety is enforced atthe wire — field numbers, unknown-field preservationcompile timetsc and nothing else
Old clients that never updatedstill work; that’s the designimpossible by construction
Repo shape it assumesmany repos, many teams, independent deploysone repo (or published types), one deploy cadence, one language
Where it breaksoperational weight: proxies, L7 balancing, no cachingthe moment a second language or a second deploy cadence appears

↔ scroll the table sideways to see every column.

Most of those rows are detail. The ones that decide it are the last four, and they say the same thing four times:

tRPC’s safety is compile-time; gRPC’s is wire-level. That distinction is invisible in a monorepo where both sides build together — tsc catches everything, and the developer experience is genuinely the best in this article. It becomes the whole story the moment the two sides stop deploying together. A mobile app in the App Store is a client you cannot recompile; version 3.2 will still be calling your API in eighteen months, and no amount of type inference reaches it. Protobuf’s field numbers exist precisely for that world: add a field, old servers ignore it, old clients round-trip it untouched.

So the honest decision procedure is one question — do the client and server ship as one unit? Yes: tRPC, and enjoy it. No: you need a contract that outlives a build.

And if the answer is “yes today, no next quarter” — a Next.js app that’s about to grow a Go service — the escape hatch is Connect, which gives you .proto contracts with browser-native calls and no proxy. It’s the option that makes the tRPC-vs-gRPC question stop mattering.

The family is forty years old

RPC was not invented in 2015. Birrell and Nelson described the whole idea — stubs, marshalling, binding — at Xerox PARC in 1984, and every system since is a re-answer to one question: where does the contract come from? There are only three answers, and they sort the entire family into three lanes.

Three lineages of RPC sorted by where the contract lives 19841991–982007–102013–152016–24 separate IDL + binary wire no schema text over HTTP host-language types only the RPC paper Birrell & Nelson CORBA / IIOP 1991 · IIOP 1996 Thrift · protobuf Facebook · Google gRPC · Cap'n Proto HTTP/2 · zero-copy Twirp → Connect proto, plain HTTP XML-RPC → SOAP 1998 · XML envelopes JSON-RPC 2.0 2010 · five fields LSP · MCP 2016 · 2024 Java RMI · DRb 1997 · same-language tRPC 2020 · TS inference The middle lane has no schema and won the niche nobody was competing for: protocols that strangers implement in unknown languages.
Three answers to "where does the contract live?" — a separate IDL, nothing at all, or the host language's own types. Every RPC system on this page is in one of these lanes, and the lane predicts its trade-offs better than its release date does.

Lane one: the IDL

CORBA (1991) was the maximal version — an OMG-standardised IDL, object references you could pass across the network, and an ORB in the middle brokering it all. It was genuinely ambitious and genuinely used, and it collapsed under vendor politics and complexity. But the idea won completely: Thrift (Facebook, 2007) and protobuf (Google-internal from the early 2000s, open-sourced 2008) are CORBA’s IDL with the committee removed, and gRPC (2015) is the public face of Google’s internal Stubby. Cap’n Proto (2013) came from the person who maintained protobuf v2 and asked what you’d build if you dropped the parse step entirely.

The lesson this lane keeps proving: the IDL outlives the framework. CORBA is dead; the same .proto file is readable by Stubby, gRPC, Twirp, and Connect. When you buy into this lane you’re buying a durable artifact, and the transport underneath it is swappable.

Lane two: no schema at all

XML-RPC (1998) was Dave Winer’s deliberately minimal alternative to the heavyweight distributed-object stacks of the day: a method name, some params, XML over HTTP POST, and a spec you could read in one sitting. Then SOAP grew out of it and became the thing XML-RPC was a reaction against, which should tell you something about how this cycle works.

JSON-RPC 2.0 (2010) is the survivor, and it’s the sleeper hit of this entire article. The spec is about five fields — jsonrpc, method, params, id, result/error — with no transport requirement whatsoever. It runs over stdio, pipes, WebSockets, anything. And that turns out to be exactly right for a niche nobody was competing for: protocols that strangers implement in languages you’ll never see.

None of those could have been gRPC. When your protocol’s job is to be implemented by a stranger over a pipe in a week, “no codegen, no HTTP/2, no toolchain” is the killer feature. JSON-RPC won by being small enough to reimplement from the spec in an afternoon.

Lane three: the host language’s types

Java RMI (1997) and Ruby’s DRb let you call methods on remote objects with the host language’s own type system and no separate schema. They’re the ancestors of tRPC’s core idea, and they died as public interfaces for two reasons. One is obvious: both ends must be the same language. The other is a security disaster you should know about — RMI’s Java serialization, Ruby’s Marshal, and Python’s pickle are all deserialization-RCE engines. Feeding untrusted bytes to any of them is remote code execution via gadget chains, not a bug to be patched. If you ever meet DRb or Pyro in production, the rule is simple: it must never be reachable from anywhere you don’t fully control.

tRPC gets the good part of this lane without the bad part, because it doesn’t serialize objects — it ships plain JSON and only borrows the types at compile time.

The whole field

RPCYearContractWire + transportStreamingWhere you’ll actually meet it
gRPC2015.protoprotobuf / HTTP2unary + 3 streaming modesinternal microservices, Kubernetes, service meshes
Connect2022.protoprotobuf or JSON / HTTP1.1+all 4; bidi needs H/2browser clients that want proto contracts
Twirp2018.protoprotobuf or JSON / HTTP1.1+ noneGo shops that found gRPC too heavy
tRPC2020TS typesJSON / any HTTPsubscriptionsNext.js / full-stack TS monorepos
JSON-RPC 2.02010noneJSON / anything, incl. stdionot in specLSP, MCP, Ethereum, Bitcoin Core (2.0 only since v28)
XML-RPC1998noneXML / HTTPWordPress xmlrpc.php, old blog APIs
SOAP1998–99WSDL (1.1 in 2001)XML / HTTP (SMTP is a separate Note)banking, telco, insurance, government
Thrift2007.thriftbinary · compact · JSON / TCP or HTTPfbthrift onlyMeta, Hadoop-adjacent, polyglot legacy
Cap’n Proto2013.capnpzero-copy / anythingflow-controlled (C++ only) + pipeliningedge runtimes, storage engines, HFT
CORBA / IIOP1991 (IIOP 1996)OMG IDLCDR / IIOPtelecom, defence, air-traffic control
Java RMI1997Java interfacesJava serialization / JRMPlegacy JEE — never expose it
Dubbo · bRPC · Kitex · Tars2011–2021IDL — except Dubbo’s classic Java interfacesbinary / TCP (Dubbo3, bRPC, Kitex)Alibaba, Baidu, ByteDance, Tencent scale
RSocket2015noneany / TCP, WS with backpressurereactive Java stacks
DRb · Pyro1990shost objectsMarshal / pickleRuby/Python internals — RCE if exposed

↔ scroll the table sideways to see every column.

Two things jump out of that table. The Chinese-ecosystem frameworks — Dubbo, bRPC, Kitex, Tars — run at volumes that dwarf most of the Western list and are almost entirely absent from English-language comparisons; if you work with those platforms, that row is your row. Dubbo is also the interesting exception to the three-lane taxonomy above: its classic protocol defines services with Java interfaces, which puts it in lane three next to RMI, and only Dubbo3’s Triple protocol moved it into lane one with protobuf. And RSocket is the only entry that treats backpressure as a first-class protocol concern rather than something you bolt on when a fast producer melts a slow consumer.

Why every generation rebuilds Twirp

There’s a cycle in that timeline, and once you see it you can’t unsee it:

  1. Someone builds the maximal RPC — every feature, its own IDL, its own transport, its own object model. CORBA. Then gRPC.
  2. Adoption is real but the operational weight is heavy. Proxies, brokers, toolchains, “why can’t I curl this.”
  3. Someone strips it back to POST a body over plain HTTP. XML-RPC after CORBA. Twirp, then Connect, after gRPC.
  4. The stripped-back one takes the mainstream, and the maximal one keeps the workloads that genuinely needed it.

Twirp said it out loud in 2018 — same protobuf contracts, plain HTTP/1.1, curl-able, no HTTP/2 requirement — and Connect is that argument won properly. Which means “gRPC vs everything” often resolves to: keep the .proto, drop the transport requirement. The contract was the valuable half all along.

The mistake every generation makes

Now the part that outranks all of it. In 1994, four engineers at Sun published A Note on Distributed Computing, and it is still the most useful thing ever written about RPC. Their argument: you cannot make a remote call look like a local one, and every attempt to do so fails in the same four places — latency, memory access, partial failure, and concurrency.

Partial failure is the one that kills you. A local function call either returns or takes the whole process down with it; there is no third outcome. A remote call has a third outcome, and it’s the common one: you don’t know.

A timed-out RPC has two indistinguishable causes: the request was lost, or the response was One timeout, two worlds the client cannot tell apart A · the request was lost Client Server The call never ran. Retrying is correct and safe. B · the response was lost Client Server charged the card The call already ran. Retrying charges it twice. TIMEOUT identical evidence Retry and you may double-charge. Don't retry and you may have dropped it. The fix is not a better protocol — it's an idempotency key. The client generates a stable request ID; the server dedups it within a window. At-least-once delivery + dedup = exactly-once processing.
No RPC framework on this page can tell world A from world B — the information genuinely isn't at the client. Retry policy is an application decision, and it needs a key.

This is why the honest RPC systems put the network in your face and the dishonest ones hide it:

And the corollary that catches people every time: exactly-once delivery does not exist. You get at-most-once (never retry — the call happens zero or one times) or at-least-once (retry until acknowledged — duplicates are guaranteed eventually). “Exactly-once” is only ever achievable as processing semantics, and only by making the operation idempotent with a stable request ID and a dedup window. If your payments endpoint doesn’t have one, you don’t have a protocol problem — you have an outage waiting for a network blip.

Choosing, in one table

SituationPick
Two services, two languages, you own bothgRPC, or Connect if a browser is involved
One TypeScript repo, client and server deploy togethertRPC
Clients you don’t control — mobile apps, partners, anything in an app storeanything with a wire-level contract; never compile-time-only
A protocol strangers will implement in unknown languagesJSON-RPC 2.0
Go shop that wants protobuf without HTTP/2Twirp or Connect
Chatty object-graph traversal over a WANCap’n Proto — promise pipelining collapses the round trips
A fast producer and a slow consumerRSocket — backpressure in the protocol
A counterparty handed you a WSDLSOAP. That’s the whole decision.
The caller can’t afford to fail when the callee is downnot RPC — see the async section of the companion piece

TL;DR

TL;DR: gRPC vs tRPC is a category error — one is a cross-language wire protocol with a schema compiler, the other is a TypeScript inference trick with no protocol at all. The only question that separates them: do client and server ship as one deployable? Yes → tRPC, and it’s a joy. No → you need a contract that survives a build, because tRPC’s safety is compile-time and gRPC’s is wire-level, and compile-time safety can’t reach a mobile app that shipped last year. Growing from one into the other → Connect: .proto contracts, browser-native, no proxy.

The wider family sorts into three lanes by where the contract lives. A separate IDL (CORBA → Thrift/protobuf → gRPC → Connect), where the IDL reliably outlives the framework. No schema (XML-RPC → JSON-RPC), which quietly won every protocol that strangers must implement over a pipe — LSP, MCP, Ethereum. Host-language types (Java RMI, DRb, tRPC) — and note that RMI/Marshal/pickle are deserialization-RCE engines that must never face untrusted input.

Watch the cycle: someone builds the maximal RPC, someone else strips it to POST-over-plain-HTTP, and the stripped one takes the mainstream. CORBA → XML-RPC. gRPC → Twirp → Connect.

And the thing every generation relearns, from A Note on Distributed Computing (1994): you cannot make a remote call look local. Partial failure is the reason — a timeout can’t tell you whether the call ran, so exactly-once delivery doesn’t exist. You get at-most-once or at-least-once, and “exactly once” is only ever processing semantics bought with an idempotency key and a dedup window. No framework on this page gives you that for free.

Sources

api design