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
| gRPC | tRPC | |
|---|---|---|
| What it actually is | a wire protocol + schema compiler | a TypeScript type-inference library |
| Contract lives in | a .proto file, versioned as an artifact | your server’s source code |
| Codegen | yes — stubs per language | none — the client imports the server’s types |
| Languages | 13 with official guides | TypeScript. That’s the list. |
| Wire format | Protobuf binary | JSON (or superjson) |
| Transport | HTTP/2 (HTTP/3 only in grpc-dotnet) | any HTTP; it doesn’t define a protocol |
| Streaming | unary + 3 streaming modes, first-class | subscriptions over WS/SSE |
| Browser | needs grpc-web + a proxy | native — it is a browser client |
| Type safety is enforced at | the wire — field numbers, unknown-field preservation | compile time — tsc and nothing else |
| Old clients that never updated | still work; that’s the design | impossible by construction |
| Repo shape it assumes | many repos, many teams, independent deploys | one repo (or published types), one deploy cadence, one language |
| Where it breaks | operational weight: proxies, L7 balancing, no caching | the 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.
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.
- LSP (2016) — every editor talking to every language server, JSON-RPC over stdio (or pipes and sockets).
- MCP (2024) — the Model Context Protocol wiring AI tools to data sources, JSON-RPC 2.0, and explicitly inspired by LSP.
- Ethereum and Bitcoin Core — node APIs, JSON-RPC.
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
| RPC | Year | Contract | Wire + transport | Streaming | Where you’ll actually meet it |
|---|---|---|---|---|---|
| gRPC | 2015 | .proto | protobuf / HTTP2 | unary + 3 streaming modes | internal microservices, Kubernetes, service meshes |
| Connect | 2022 | .proto | protobuf or JSON / HTTP1.1+ | all 4; bidi needs H/2 | browser clients that want proto contracts |
| Twirp | 2018 | .proto | protobuf or JSON / HTTP1.1+ | ✗ none | Go shops that found gRPC too heavy |
| tRPC | 2020 | TS types | JSON / any HTTP | subscriptions | Next.js / full-stack TS monorepos |
| JSON-RPC 2.0 | 2010 | none | JSON / anything, incl. stdio | not in spec | LSP, MCP, Ethereum, Bitcoin Core (2.0 only since v28) |
| XML-RPC | 1998 | none | XML / HTTP | ✗ | WordPress xmlrpc.php, old blog APIs |
| SOAP | 1998–99 | WSDL (1.1 in 2001) | XML / HTTP (SMTP is a separate Note) | ✗ | banking, telco, insurance, government |
| Thrift | 2007 | .thrift | binary · compact · JSON / TCP or HTTP | fbthrift only | Meta, Hadoop-adjacent, polyglot legacy |
| Cap’n Proto | 2013 | .capnp | zero-copy / anything | flow-controlled (C++ only) + pipelining | edge runtimes, storage engines, HFT |
| CORBA / IIOP | 1991 (IIOP 1996) | OMG IDL | CDR / IIOP | ✗ | telecom, defence, air-traffic control |
| Java RMI | 1997 | Java interfaces | Java serialization / JRMP | ✗ | legacy JEE — never expose it |
| Dubbo · bRPC · Kitex · Tars | 2011–2021 | IDL — except Dubbo’s classic Java interfaces | binary / TCP | ✓ (Dubbo3, bRPC, Kitex) | Alibaba, Baidu, ByteDance, Tencent scale |
| RSocket | 2015 | none | any / TCP, WS | ✓ with backpressure | reactive Java stacks |
| DRb · Pyro | 1990s | host objects | Marshal / pickle | ✗ | Ruby/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:
- Someone builds the maximal RPC — every feature, its own IDL, its own transport, its own object model. CORBA. Then gRPC.
- Adoption is real but the operational weight is heavy. Proxies, brokers, toolchains, “why can’t I
curlthis.” - Someone strips it back to POST a body over plain HTTP. XML-RPC after CORBA. Twirp, then Connect, after gRPC.
- 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.
This is why the honest RPC systems put the network in your face and the dishonest ones hide it:
- Java RMI forced
RemoteExceptioninto every remote signature. Everyone found it annoying; it was right. - CORBA tried hardest to make a remote object look like a local one, and its complexity is the exact price of that ambition.
- gRPC’s deadline propagation is the modern answer — don’t pretend the hop isn’t there, give it an explicit budget that downstream calls inherit. (Automatic in some implementations; in others, Go included, you thread the context through yourself.)
- tRPC splits the difference well: the types are local, the call is unmistakably async. It never claims the network vanished.
- Erlang/OTP is the most honest of all: remote calls can fail, so you get links, monitors and supervisors as language-level machinery for the failure you’re guaranteed to have.
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
| Situation | Pick |
|---|---|
| Two services, two languages, you own both | gRPC, or Connect if a browser is involved |
| One TypeScript repo, client and server deploy together | tRPC |
| Clients you don’t control — mobile apps, partners, anything in an app store | anything with a wire-level contract; never compile-time-only |
| A protocol strangers will implement in unknown languages | JSON-RPC 2.0 |
| Go shop that wants protobuf without HTTP/2 | Twirp or Connect |
| Chatty object-graph traversal over a WAN | Cap’n Proto — promise pipelining collapses the round trips |
| A fast producer and a slow consumer | RSocket — backpressure in the protocol |
| A counterparty handed you a WSDL | SOAP. That’s the whole decision. |
| The caller can’t afford to fail when the callee is down | not 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:
.protocontracts, 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/pickleare 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
- Implementing Remote Procedure Calls — Birrell & Nelson, ACM TOCS, February 1984
- A Note on Distributed Computing — Waldo, Wyant, Wollrath & Kendall, Sun Labs TR-94-29, 1994 (PDF)
- JSON-RPC 2.0 — specification
- Language Server Protocol — base protocol (JSON-RPC over stdio)
- Model Context Protocol — specification
- gRPC — deadlines and deadline propagation
- Twirp — a simpler RPC framework (Twitch)
- Connect — protocol reference (Buf)
- tRPC — concepts and end-to-end type inference
- Cap’n Proto — promise pipelining
- Apache Thrift — documentation
- CORBA / IIOP — OMG specification
- Why deserializing untrusted input is RCE — OWASP Deserialization Cheat Sheet (Java, .NET, PHP, Python)
- Ruby
Marshal— “never unmarshal user supplied input” · Pythonpickle— “is not secure” - gRPC — supported languages
- RSocket — protocol with application-level backpressure
- Apache Dubbo · bRPC · CloudWeGo Kitex · Tars (Linux Foundation)