RPC in Practice: How to Make Your Services Talk Without the Headaches
Understand RPC (Remote Procedure Call), compare gRPC, tRPC, and JSON-RPC, and discover when to use each one in your microservices architecture.
Thiago Saraiva

Have you ever tried to make two services talk to each other and felt like you were reinventing the wheel? Crafting HTTP requests by hand, defining REST routes, serializing and deserializing JSON everywhere... Now imagine if calling a function on another server were as simple as calling a local function. That's the promise of RPC.
What Is RPC, Anyway?
RPC (Remote Procedure Call) is a communication paradigm where the client calls a remote function as if it were local. Instead of thinking "make a POST to /api/users", you think "call the function getUser(id)".
The fundamental difference from REST is the mental model:
- REST: resource-oriented ("I want the resource /users/123")
- RPC: action-oriented ("I want to execute getUser with id 123")
Mental model: REST is ordering from a restaurant menu. You point at a resource ("the steak, medium rare") and the kitchen knows what to do. RPC is calling a contractor and saying "rewire the kitchen outlet." You're invoking a specific action, not picking from a catalog. Different mental models, both perfectly valid. The trick is knowing which one fits the conversation you're trying to have.
In practice, the flow looks like this:
Client calls function → Stub serializes → Network → Server deserializes → Executes → Response comes back
A Quick Trip Through RPC History
RPC isn't new. It's been quietly evolving since before most of us wrote our first line of code:
- CORBA (90s): the original horror story. Cross-language object invocation that promised the world and delivered XML configs, IDL compilers, and engineers crying at 2am.
- XML-RPC (late 90s): "what if we just used XML over HTTP?" Simple, but verbose enough to make your network cry.
- SOAP (2000s): XML-RPC's ambitious cousin. WSDLs, envelopes, namespaces. Enterprise loved it. Developers tolerated it.
- Thrift (2007, Facebook): binary serialization, multiple languages, actually pleasant. Built because Facebook needed services to talk fast across stacks.
- gRPC (2015, Google): open-sourced from Google's internal Stubby. Protocol Buffers + HTTP/2 = the current heavyweight champion.
Each generation existed because the previous one was either too slow, too verbose, or too tied to one ecosystem. Knowing this history saves you from reinventing painful wheels.
REST vs RPC: When to Use Each?
This is the million-dollar question. Here's my take:
Use REST when:
- Your API is public and needs to be easily consumed by third parties
- You want to leverage native HTTP caching
- Resources are well-defined and operations are standard CRUD
Use RPC when:
- Communication between internal microservices
- Performance is critical (gRPC uses Protocol Buffers and HTTP/2)
- You want strong type safety between client and server
- Operations are more "actions" than "resources"
gRPC: The Heavyweight of Service-to-Service Communication
gRPC, created by Google, uses Protocol Buffers for serialization and HTTP/2 for transport. The result? Absurdly fast communication.
Fun fact: Google has been running an internal RPC framework called Stubby for over a decade before open-sourcing the lessons as gRPC. We're talking trillions of RPCs per second across their datacenters. When Netflix migrated parts of its inter-service communication to gRPC, they reported latency drops in the 60-70% range for certain payloads. That's not marketing fluff, that's the binary-vs-JSON tax made visible.
You start by defining your service in a .proto file:
From this, gRPC automatically generates client and server code. In Node.js, the server looks like this:
gRPC supports 4 types of communication:
- Unary: classic request-response
- Server streaming: server sends multiple responses
- Client streaming: client sends multiple requests
- Bidirectional streaming: both send and receive simultaneously
The Honest gRPC Pain Points
Before you fall in love, know what you're signing up for:
- Browsers don't speak gRPC natively. You need gRPC-Web plus a proxy (Envoy, usually). Suddenly your "simple" stack has a sidecar.
- Debugging is harder. No
curl localhost:50051/usersto poke around. You needgrpcurl, BloomRPC, or Postman's gRPC mode. Binary payloads don't render in browser DevTools. - The .proto learning curve is real. Field numbers, oneof, well-known types, backwards-compatibility rules. Break the wrong rule and you'll silently corrupt data in production.
- Tooling assumes you have infrastructure. Load balancers that understand HTTP/2 trailers, observability that decodes protobuf, etc.
None of these are dealbreakers. They're just the price of admission.
tRPC: Type Safety from Backend to Frontend
If you work with full-stack TypeScript, tRPC is a revolution. It eliminates the need to define separate schemas -- your backend types flow automatically to the frontend.
Think of it as wormhole-typing: the same TypeScript brain on both ends of the wire, no schema translator in between.
The best thing about tRPC: change a type on the backend and TypeScript will immediately complain on the frontend. Zero runtime overhead, zero code generation.
JSON-RPC: Simplicity Above All
JSON-RPC is the simple cousin of the family. It's a lightweight protocol that works over HTTP or WebSocket:
It's perfect when you want RPC without the complexity of gRPC and without being tied to tRPC's TypeScript ecosystem.
Comparing in Practice
Here's my decision table:
- gRPC: high-performance communication between microservices, especially in polyglot systems (Go, Java, Python, Node.js)
- tRPC: full-stack TypeScript applications (Next.js, React + Node.js)
- JSON-RPC: simple APIs, internal tools, or when you need something lightweight
- REST: public APIs, third-party integrations, when HTTP caching matters
Key Takeaways
RPC is not a replacement for REST -- they are different tools for different problems. If you're building internal microservices, gRPC will give you performance and strong contracts. If you're in the TypeScript world, tRPC is almost magical.
My recommendation: start with REST for public APIs and explore RPC for internal communication. When complexity or performance requirements demand it, you'll naturally feel that RPC makes more sense.
The most important thing is understanding the trade-off: RPC gives you a cleaner and more performant abstraction, but it couples client and server more tightly. Choose consciously.
FAQ
Does tRPC work outside TypeScript? No, and that's the point. tRPC's whole superpower is leaning on the TS compiler to share types across the wire. If your client is Swift, Kotlin, or Python, you want gRPC or JSON-RPC instead.
What about gRPC on Bun or Deno?
Bun has improving but still incomplete @grpc/grpc-js compatibility (mostly works, occasional FFI quirks). Deno supports it through npm specifiers and Connect-ES works particularly well there. For greenfield Bun/Deno projects, Connect is often the smoother bet.
Can I run gRPC over the public internet? Technically yes, practically be careful. HTTP/2 and TLS handle the transport fine, but many corporate proxies, CDNs, and WAFs still mishandle gRPC trailers. For public-facing endpoints, gRPC-Web or Connect with HTTP/1.1 fallback is more pragmatic.
Does microservices + tRPC make sense? Only if every service is TypeScript and you control both ends. The moment one team writes Go, the type-sharing magic evaporates. For polyglot microservices, gRPC is the boring correct answer.
What about Connect protocol from Buf? Connect is the "gRPC without the rough edges" project. Same .proto definitions, but speaks HTTP/1.1, HTTP/2, and gRPC, works in browsers without a proxy, and supports plain JSON for debugging with curl. If gRPC's pain points are blocking you but you want its contract-first discipline, Connect is worth a serious look.