A remote procedure call (RPC) is a mechanism that lets a program call a procedure on another computer as if it were a local function call. The caller writes result = getBalance(accountNo) and the RPC runtime hides everything else: finding the server, packing the arguments into a message, sending them over the network, running the procedure there, and bringing the return value back. The idea was formalised by Birrell and Nelson in 1984 and it is still the model behind NFS, most microservice traffic, and gRPC.
The important thing to understand about RPC is that the transparency is deliberate but incomplete. A remote call can fail in ways a local call never can, and the whole subject is really about what happens when it does.
What problem does RPC solve?
Before RPC, two programs on different machines talked through raw sockets. The programmer had to open a connection, decide a byte format, serialise every integer and string by hand, define a message header so the receiver knew where one message ended, handle partial reads, and write the matching decoder on the other side. All of that is plumbing, and every project wrote it again.
RPC pushes that plumbing into generated code. What the programmer gets is:
- Access transparency: a remote call looks syntactically like a local one.
- Location transparency: the client does not hard-code where the procedure runs; a name service or load balancer can move it.
- Language independence: a Python client can call a Java or Go server, because the message format is defined separately from both.
- Type safety: the argument and return types are checked against one shared definition.
What RPC does not hide is latency and failure. A local call takes nanoseconds and cannot be lost. A remote call in a data centre takes hundreds of microseconds to a few milliseconds, and over the public internet tens to hundreds of milliseconds. It can also vanish halfway. Treating an RPC as free is the classic design mistake.
How does RPC work? Step-by-step

Here is the full round trip. Steps 1 and 10 are the only parts the application programmer writes; everything between is done by generated stubs and the RPC runtime.
- The client calls the local stub. The client program invokes what looks like an ordinary procedure. The call goes onto its own stack in the usual way, so the client is now blocked, waiting for a return.
- The client stub marshals the arguments. Marshalling means converting the parameters from their in-memory representation into a flat byte stream that any machine can decode: fixing byte order, encoding integers and floats to an agreed format, and flattening structures. The stub also adds a header with the procedure identifier and a unique call ID.
- The stub hands the message to the local RPC runtime, which locates the server. With ONC RPC this means asking the portmapper on port 111 which port the service is on; in a modern deployment it means a service registry or a DNS name behind a load balancer.
- The operating system sends the message over the transport, usually TCP, sometimes UDP for small idempotent calls, and HTTP/2 in the case of gRPC.
- The server’s operating system receives the packet and passes it to the waiting RPC runtime, which dispatches it to the right service.
- The server stub (skeleton) unmarshals the arguments back into local data structures, checking that the types and the sizes match what the procedure expects.
- The skeleton calls the actual server procedure with those arguments. From the server procedure’s point of view this is an ordinary local call made by an ordinary local caller.
- The procedure returns a value to the skeleton, which marshals the result, or an error code, into a reply message carrying the same call ID.
- The reply travels back across the network to the client runtime, which matches the call ID to the blocked call.
- The client stub unmarshals the result and returns it to the client program, which unblocks and carries on as if nothing had happened.
Count the context switches and the copies and you can see why an RPC is thousands of times more expensive than a local call even when the network is perfect.
Stubs, skeletons and the IDL
Three pieces of generated machinery make the transparency possible.
The interface definition language (IDL) is a small language in which you declare the service: the procedure names, their parameter types and their return types. It is deliberately not a programming language, because both sides must agree on it without agreeing on an implementation language. Sun RPC uses XDR, CORBA uses OMG IDL, DCOM uses MIDL, and gRPC uses protocol buffers.
The client stub is the generated proxy on the caller’s machine. It exposes the same signature as the remote procedure, marshals arguments, sends the request, waits, unmarshals the reply, and raises an exception if the call failed. It contains no business logic.
The server stub, also called the skeleton, is the mirror image. It listens, unmarshals, dispatches to the real procedure, and marshals the result back.
You compile the IDL file once with a stub generator, which emits both. Change the IDL and you regenerate both, which is exactly why version mismatches between client and server are a standing hazard in RPC systems.
Marshalling and why you cannot pass a pointer
Marshalling converts data into a machine-independent byte sequence; unmarshalling reverses it. The reason it cannot be skipped is that machines differ: byte order can be little-endian or big-endian, an integer can be 32 or 64 bits wide, and padding inside a structure differs by compiler.
This forces a real limitation. RPC parameters are passed by value, not by reference. A pointer is an address inside one process’s memory and is meaningless in another process on another machine. So you cannot pass a linked list by handing over its head pointer; you must serialise the whole structure. Some systems offer call-by-copy/restore, where the referenced data is copied out and copied back, but that changes behaviour whenever the same data is aliased. Global variables are similarly out of reach.
RPC call semantics: at-least-once, at-most-once, exactly-once
When a client sends a request and no reply comes back, it cannot tell whether the request was lost, the server crashed before running the procedure, the server ran it and then crashed, or the reply was lost. Those four cases need different responses, and the client cannot distinguish them. The choice a system makes is called its call semantics.
| Semantics | What the runtime does | Result if things fail | Used by |
|---|---|---|---|
| Maybe (best effort) | Send once, do not retry | Procedure runs zero or one time; caller may never know | Unreliable logging, telemetry |
| At-least-once | Retransmit until a reply arrives | Procedure may run more than once, so it must be idempotent | Sun RPC over UDP, NFS reads |
| At-most-once | Retransmit, but the server keeps a reply cache keyed by call ID and filters duplicates | Procedure runs once or not at all; caller gets an error if not | DCE RPC, Java RMI, gRPC in practice |
| Exactly-once | Would guarantee one execution and one reply in every case | Not achievable by the RPC layer alone | Approximated by the application |
Why exactly-once is hard. Suppose the server executes the procedure, commits the change, and then crashes before the reply is sent. The client times out and retries. For the retry to be safe, the server must remember, across the crash, that call ID 7231 was already executed and what it returned. That means writing the call ID and the result to stable storage inside the same atomic transaction as the business change, then keeping that record until the client can no longer retry. Add a client that also crashes and restarts with a new identity, or a network that delivers a duplicate an hour late, and the bookkeeping grows without limit. The theoretical obstacle is the same one behind the two-generals problem: no finite exchange of messages over a lossy link lets both sides agree with certainty.
What real systems do instead: make the operation idempotent so that running it twice is harmless, and carry an idempotency key supplied by the client. A payment API that accepts the same key twice returns the first result rather than charging again. That gives exactly-once effects on top of at-least-once delivery, which is the only honest version of the guarantee.
Student tip: “read the balance” is naturally idempotent, “set the balance to 5000” is idempotent, and “add 500 to the balance” is not. Most retry bugs in distributed systems come from retrying the third kind.
RPC failure modes
The classic list of five, and what a runtime does about each:
- The client cannot locate the server. The service is down, or the version it wants no longer exists. The stub raises an exception; there is nothing to retry against.
- The request message is lost. Detected by a timer in the client runtime, handled by retransmission.
- The server crashes after receiving the request. The client cannot tell whether the work was done. This is the case that forces the semantics choice above.
- The reply message is lost. Indistinguishable from the previous case at the client. A reply cache on the server lets a duplicate request be answered without re-executing.
- The client crashes after sending. The server is left computing a result nobody will read, called an orphan call. Remedies are extermination (log calls and kill orphans on restart), reincarnation (the client’s new epoch number invalidates old calls), and expiration (each call gets a time budget and is killed when it runs out).
Two more that bite in production: partial failure, where one call in a chain of five fails and leaves the system half-updated, and cascading timeouts, where a slow service causes every caller to hold threads open until the whole system stalls. Circuit breakers, deadlines propagated along the call chain, and bounded retries with exponential backoff are the standard defences.
RPC vs REST vs message queues
| Aspect | RPC (gRPC, Sun RPC) | REST over HTTP | Message queue (Kafka, RabbitMQ) |
|---|---|---|---|
| Mental model | Call a function | Act on a resource with GET, POST, PUT, DELETE | Publish an event, someone consumes it later |
| Coupling | Tight: client and server share an interface definition | Looser: shared URL and JSON shape | Loosest: producer does not know the consumer |
| Timing | Synchronous by default, caller blocks | Synchronous request-response | Asynchronous, consumer can be offline |
| Payload | Binary, compact (protocol buffers) | Text JSON, human readable, larger | Any, commonly JSON or Avro |
| Contract | IDL file, compiler-checked on both sides | OpenAPI spec, checked at runtime if at all | Schema registry, checked by convention |
| Streaming | Yes, in both directions | Awkward, needs SSE or WebSockets | Natural, it is a log |
| Browser support | Needs a proxy layer | Native | Not applicable |
| Best for | Internal service-to-service calls where latency matters | Public APIs and anything a browser calls | Work that can be deferred, fan-out, buffering load spikes |
A useful way to choose: if the caller needs the answer before it can continue, use RPC or REST. If it only needs the work to happen eventually, use a queue, because that also absorbs traffic spikes and survives the consumer being down.
gRPC and protocol buffers: the modern form
gRPC, released by Google in 2015, is the RPC framework most new systems use. Three parts matter.
Protocol buffers are the IDL and the wire format. You write a .proto file declaring messages and a service, and the compiler generates stubs for Java, Go, Python, C++, C#, Kotlin, Node and others. Each field carries a small tag number instead of its name, so the encoded message is a fraction of the size of the equivalent JSON and parses faster. Fields are optional by tag number, which is what lets a new server talk to an old client as long as you only add fields and never reuse a tag.
HTTP/2 is the transport. It multiplexes many calls over one TCP connection, so there is no head-of-line blocking between calls and no repeated connection setup, and it compresses headers.
Four call patterns instead of one: unary (one request, one reply), server streaming (one request, a stream of replies, as in a live price feed), client streaming (a stream of uploads, one reply), and bidirectional streaming (both at once, as in a chat or a telemetry link).
gRPC also standardises deadlines, cancellation, per-call metadata, and interceptors for authentication and tracing, which is a large part of why it displaced the earlier XML-RPC and JSON-RPC styles for internal traffic. JSON-RPC is still common where a plain text protocol over HTTP is easier to debug or embed.
Where RPC is used
- NFS, the Network File System, is built on Sun ONC RPC; every read and write of a mounted remote directory is an RPC.
- Microservices. Inside one company’s data centre, service A calling service B is almost always an RPC, increasingly gRPC.
- Distributed databases and cluster managers, where nodes exchange heartbeats, leader elections and replication requests as RPCs.
- Windows internals, where MSRPC carries a great deal of local and remote system communication.
- Java RMI, which is object-oriented RPC: you call a method on a remote object reference.
References
- A. D. Birrell and B. J. Nelson, “Implementing Remote Procedure Calls”, ACM Transactions on Computer Systems, 1984.
- Multi-tier model with JSON-RPC in telemedicine devices, ResearchGate.
- NPTEL – Distributed Systems, IIT/IISc.
- AICTE Model Curriculum – Computer Networks.
FAQs
What is a remote procedure call (RPC)?
A remote procedure call is a mechanism that lets a program execute a procedure on another computer as though it were a local function call. The RPC runtime and generated stubs handle finding the server, packing the arguments, sending them over the network and returning the result.
What are the steps in an RPC call?
The client calls the stub; the stub marshals the arguments into a message; the runtime locates the server and sends it; the server stub unmarshals the arguments and calls the real procedure; the result is marshalled into a reply, sent back, unmarshalled by the client stub and returned to the caller.
What is the difference between a stub and a skeleton?
The stub sits on the client and acts as a proxy for the remote procedure: it marshals arguments and unmarshals the reply. The skeleton, or server stub, sits on the server, unmarshals the request, calls the real procedure and marshals the result. Both are generated from the same interface definition file.
Why is exactly-once RPC semantics difficult?
Because a client that gets no reply cannot tell whether the server never received the request, crashed before running it, crashed after running it, or simply lost the reply. Guaranteeing one execution would need the server to record every call ID and result in stable storage atomically with the work itself, and keep it forever. Systems approximate it with idempotent operations plus an idempotency key.
Is gRPC the same as RPC?
gRPC is one implementation of the RPC idea, not the idea itself. It uses protocol buffers as its interface definition language and wire format and HTTP/2 as its transport, and it adds streaming, deadlines and cancellation. Sun RPC, DCE RPC, Java RMI, XML-RPC and JSON-RPC are other implementations.
