You write one modest line of code:
It looks exactly like calling a function you wrote ten lines up. But add does not live in
your program. It lives on a machine in another building — maybe another continent. When your
line runs, the numbers result get its value. Yet in your source, none of that shows. It reads like an
ordinary call.
That illusion is a Remote Procedure Call, or RPC: the idea that
invoking code on another machine should look and feel like calling a function on your
own. Instead of making the programmer hand-write socket code, serialise bytes, and parse
replies, RPC hides the whole
How can add(2, 3) possibly look local when the real add is elsewhere? The
secret is a piece of stand-in code called a stub. On the caller's side there is a
client stub — a fake add with the right name and signature that the
programmer calls without a second thought. It looks like the function; it is really an impostor whose
only job is to smuggle the call across the network.
When you call the client stub, four things happen in order:
add — an ordinary local call,
on that machine.
result.
Neither the programmer who wrote add(2, 3) nor the programmer who wrote the real
add had to think about bytes, packets, or ports. The two stubs did all the packing and
unpacking; the network sat invisibly in the middle. Follow one round trip below, step by step.
To see the mechanism with nothing hidden, here is a whole RPC squeezed into a single process — no real
network, just the same shape. There is a marshal/unmarshal pair (JSON
standing in for the wire format), a transport that would be the network, a
client stub that serialises a call, and a server dispatcher that
unmarshals it, runs the real function, and marshals the reply. Press Run ▶ and watch
the bytes go across and come back.
The programmer's whole world is the last three lines: stub("add", 2, 3) returns
Making a remote call look local is a beautiful convenience, and also a quiet trap. A local call and a remote call are alike on the surface and profoundly different underneath. Wherever the two differ, the abstraction leaks — the network reality seeps through the pretty function call, and code written as if the call were local behaves badly.
These are not bugs in any particular RPC library; they are the unavoidable difference between "in this process" and "across a network." The danger is precisely that the syntax hides them. This tension is old and famous — the "Fallacies of Distributed Computing" begin with programmers assuming the network is reliable, instant, and free, exactly because RPC let them pretend it was.
This is the seductive misconception at the heart of RPC, and believing it will burn you. RPC makes a remote call look like a local one — same syntax — but it can never make it behave like one. A local call does not spend milliseconds on the wire, does not vanish into a network partition, cannot execute "somewhere between zero and two times." If you write remote calls as though they were free and infallible — chatty loops of tiny calls, no timeouts, no retry policy, sharing objects by reference in your head — the leak becomes a flood. Treat every RPC as what it is: a message that might not arrive, to a machine that might be gone, taking a time you cannot predict. Design for that, and RPC is a gift. Forget it, and it is a landmine.
The nastiest leak deserves its own page-worth of care. Suppose your client sends a request and then … silence. The timeout fires. Something went wrong — but what? There are two very different possibilities, and from the client's chair they look identical:
The client cannot tell these apart. All it knows is "no answer came." So what should it do — give up, or retry? That single decision is the whole subject of delivery semantics:
Now the danger becomes concrete. Suppose the remote call is transfer($100), and you chose
at-least-once (retry on timeout). The server receives the request, moves the $100, sends "done" — and
the reply is lost. Your client, hearing nothing, retries. The server moves another $100. Your
friend just got paid twice, and your account is short. The retry that saves a lost-request call
doubles a lost-reply call.
The property that rescues you is idempotency: an operation is idempotent if doing it twice has the same effect as doing it once. "Set the balance to $500" is idempotent — running it again changes nothing. "Add $100 to the balance" is not — each run adds again. Retrying an idempotent call is harmless, so at-least-once is safe for it. Retrying a non-idempotent call is a bug waiting to happen. That is why well-built RPC systems attach a unique request ID to each call: the server remembers IDs it has already processed and quietly ignores duplicates — turning an at-least-once transport into effectively exactly-once effects.
Because not retrying doesn't make the problem go away — it just swaps a double-execution risk
for a zero-execution risk. If you never retry (at-most-once) and it was the request that got
lost, the transfer simply never happened and nobody told you. Neither extreme is safe on its own; the
real fix is to make retries safe. Give each request a unique ID and have the server deduplicate: the
first time it sees ID #1734 it does the transfer and records "done for #1734"; every
retry of #1734 it recognises and returns the stored result without moving money again.
Now the client can retry freely — the transport is at-least-once, but the effect is
exactly-once. This is how real payment and messaging systems get correctness out of an unreliable
network: not by avoiding retries, but by making the operation safe to repeat.
The idea is decades old, but it is everywhere in modern systems, just under newer names. gRPC
(Google's framework) marshals with a compact binary format called Protocol Buffers and runs over HTTP/2;
you write an .proto interface and it generates your client and server stubs in a
dozen languages. JSON-RPC is a lightweight cousin that marshals calls as small JSON
objects — much like our toy above. Older relatives include Sun RPC, CORBA, and Java RMI. Even a plain
REST/HTTP API is RPC-flavoured: a request names an operation and carries marshalled arguments, and a
marshalled reply comes back. The vocabulary changes; the shape — stub, marshal, transport, unmarshal,
run, reply — does not, and neither do the leaks. Whatever the badge on the box, a call that crosses the
network is slow, can fail, passes by value, and might run more than once.