RPC: Calling a Function on Another Machine
Why this matters: RPC is the abstraction that lets you write user := userService.Get(id) when userService lives in another datacenter. It is the most productive abstraction in backend engineering — and Lesson 4 is about the bill it quietly runs up.
Key takeaway
A Remote Procedure Call executes a subroutine in a different address space — usually on a different machine — while looking like an ordinary local call. Stubs handle the illusion, marshaling handles the data, and the RPC runtime handles the network.
What an RPC is
Remote Procedure Call (RPC) is a widely used interprocess communication protocol in distributed systems. In the OSI model it spans the transport and application layers. It lets a program execute a subroutine in a separate address space, typically on a different machine.
The point is the programming model: you write a standard local procedure call and ignore the underlying remote interaction. No sockets, no byte buffers, no framing.
When a client makes a synchronous RPC, the calling thread blocks until a response arrives. The arguments are serialized, transmitted to the remote server, executed there, and the response is sent back — at which point the blocked thread resumes exactly as if it had returned from a local function.
The five components
A client-server RPC setup has five main components across two machines.
The client machine hosts the client, the client stub, and an instance of the RPC runtime. The server machine hosts the server, the server stub, and its own RPC runtime instance.
| Component | Lives on | Responsibility |
|---|---|---|
| Client | Client machine | Your calling code — makes what looks like a local call |
| Client stub | Client machine (client's address space) | Marshals parameters into a message; unmarshals the reply |
| RPC runtime | Both machines | Transmission, retransmission, acknowledgment, encryption |
| Server stub | Server machine | Unmarshals parameters; invokes the real routine; marshals the result |
| Server | Server machine | The actual procedure that does the work |
The ten steps, end to end
Spelled out:
- The client invokes the client stub, passing parameters as usual. The stub lives in the client's address space.
- The stub converts the parameters into a standardized format — marshaling — and packs them into a message, then asks the local RPC runtime to deliver it.
- The client's RPC runtime transmits the message across the network and waits for a response.
- The server's RPC runtime receives the message and passes it to the server stub.
- The server stub unmarshals the message to extract the parameters, then calls the target routine with a local procedure call.
- The server routine executes and returns its result to the server stub.
- The server stub packs the result into a message and hands it to the server's RPC runtime.
- That runtime sends the packed result back to the client's runtime.
- The client's runtime receives the result and passes it to the client stub.
- The client stub unpacks the result, and execution returns to the caller.
Marshaling and the IDL
Marshaling is the step that makes cross-machine and cross-language calls possible: an in-memory object graph becomes a flat, self-describing sequence of bytes that any language can decode.
Which requires both sides to agree on the shape in advance. That agreement is an Interface Definition Language (IDL) — a schema file that both sides compile stubs from.
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}
message GetUserRequest { string user_id = 1; }
message User { string user_id = 1; string name = 2; int64 created_at = 3; }
Compile that once and you get a client stub in Go and a server stub in Java that provably agree. The field numbers — not names — are what go on the wire, which is what makes schema evolution safe: add field 4, and old readers skip it instead of breaking.
| Format | Encoding | Schema needed | Typical size | Reach for it when |
|---|---|---|---|---|
| JSON | Text | Baseline (largest) | Public APIs, browsers, human debuggability matters | |
| Protocol Buffers | Binary | Yes (.proto) | ~3-10x smaller | Internal service-to-service; the gRPC default |
| Thrift | Binary | Yes (.thrift) | Comparable to protobuf | Polyglot fleets; bundles its own RPC stack |
| Avro | Binary | Yes (writer + reader) | Compact | Data pipelines, long-lived stored records |
gRPC and modern transport
Today's dominant RPC framework, gRPC, runs the model above over HTTP/2, which buys three things the classic design lacked:
- Multiplexing — many concurrent calls share one TCP connection, so there is no head-of-line blocking at the connection level and no per-call handshake cost.
- Streaming — a call is no longer only request/response.
- Header compression and binary framing — less per-call overhead, which matters when you make billions of calls.
| Mode | Shape | Good for |
|---|---|---|
| Unary | 1 request, 1 response | The classic RPC — fetch a user, place an order |
| Server streaming | 1 request, N responses | Feeds, large result sets, progress updates |
| Client streaming | N requests, 1 response | Uploads, batched metric ingestion |
| Bidirectional | N requests, N responses | Chat, live location, interactive sessions |
Real-world usage
RPC is not an academic construct — it is how large fleets talk:
- Google uses gRPC, its open-source high-performance framework, for communication between components in services like Google Search and YouTube.
- Uber relies on RPC for real-time operations: location tracking, ride matching, and data exchange between the driver and rider apps and the backend.
- Facebook uses Thrift for RPC and serialization, enabling interoperability across languages — a Python client can call a C++ server without either side knowing or caring.
Backend services prefer RPC for its high performance and for the simplicity of treating remote code as local functions.
Summary
RPC lets programs call functions in another process or on another machine as though they were local. That abstraction removes socket management, serialization, and transport-level retry logic from your application code, so you can think about service interactions and architecture instead of bytes on a wire.
Key takeaway
RPC hides the network beautifully — right up until the network misbehaves. It can hide latency and encoding. It fundamentally cannot hide partial failure, because only your application knows whether re-running the call is safe.
Interview signal by level
| Level | What a strong answer sounds like |
|---|---|
| L4 | "These services talk over gRPC." Knows the tool and that it's faster than REST. |
| L5 | Justifies it: "gRPC internally for the binary encoding and multiplexing; REST at the edge because browsers and partners consume it." |
| Staff+ | Owns the contract lifecycle: "protobuf gives us safe schema evolution across independent deploys — add fields, never renumber — and I'd put the IDL in a shared repo with CI compatibility checks." |
Next: what actually happens when step 3 gets no answer.