RPC Frameworks: Why Remote Calls Still Look Local


Remote Procedure Call (RPC) frameworks let developers invoke functions on remote servers as if they were local method calls. Despite decades of debate about whether this abstraction leaks too much, RPC remains the backbone of most distributed systems. The tension between transparency and explicitness shapes how modern services communicate.

The Core Abstraction

RPC frameworks generate client stubs that expose the same interface as the remote service. A call to userService.getProfile(userId) looks identical whether the implementation runs in-process or across a data center. The framework handles serialization, network transport, error marshaling, and deserialization under the hood.

This transparency reduces cognitive load. Engineers work with typed method signatures instead of raw HTTP requests or message payloads. IDL-based systems like gRPC, Thrift, and Cap’n Proto generate these stubs from a schema definition, ensuring both sides agree on the contract. Changes to the interface propagate through the type system, catching incompatibilities at compile time rather than in production.

Why Transparency Fails

The abstraction breaks down because remote calls behave fundamentally differently than local ones. Network calls are orders of magnitude slower, can fail in partial ways, and introduce latency variance that local calls never exhibit. A timeout might mean the request never arrived, the server crashed mid-execution, or the response got lost—three scenarios that require different recovery strategies.

Treating remote calls as local encourages patterns that work in-process but fail at scale. Chatty interfaces that make dozens of sequential calls become bottlenecks. Forgetting to set timeouts leads to cascading failures. Ignoring idempotency means retries corrupt state. The “fallacies of distributed computing” stem directly from pretending the network doesn’t exist.

Modern RPC frameworks acknowledge this by surfacing distributed system concerns in the API. gRPC exposes deadlines, metadata, and streaming primitives. Many frameworks require explicit error handling for network failures distinct from application errors. Context propagation threads tracing and cancellation signals through call chains, making the distributed nature visible where it matters.

The Alternatives

RESTful HTTP APIs make the network explicit—every call is obviously remote, and developers think in terms of resources and status codes rather than method invocations. GraphQL clients compose queries declaratively, batching multiple data fetches into a single round trip. Message queues decouple producers and consumers entirely, trading synchronous semantics for durability and loose coupling.

Each alternative solves problems RPC creates but introduces its own complexity. REST lacks type safety and requires manual serialization. GraphQL adds query parsing overhead and shifts complexity to the schema. Message queues turn simple request-response patterns into asynchronous state machines.

Why RPC Persists

RPC wins on developer ergonomics for service-to-service communication. Typed interfaces catch errors early. Generated clients eliminate boilerplate. Streaming RPC supports efficient bulk transfers and real-time updates without dropping to raw sockets. Binary protocols like Protocol Buffers compress payloads far better than JSON while remaining evolvable.

The frameworks that succeed balance transparency with pragmatism. They make simple cases feel local while exposing the levers needed to handle distributed system realities—retries, deadlines, load balancing, circuit breaking. Engineers get the productivity of local calls when the network cooperates and the control to handle failures when it doesn’t.

The debate over RPC transparency misses the point. The abstraction isn’t about hiding the network—it’s about choosing where to pay the complexity cost. RPC pushes distributed system concerns into infrastructure code and lets application logic stay focused on business semantics. That tradeoff still makes sense for most internal service meshes, even if it wouldn’t for public APIs or high-fanout edge calls.