IPC, RPC, and Communication Boundaries
LESSON
IPC, RPC, and Communication Boundaries
By the end of this lesson, you will be able to...
Trace one request from a process to a local helper and identify the states where it can wait, fail, or leave work unfinished.
Choose between a local call, IPC, and a remote call from the isolation and completion promise required.
Explain why a timeout reports a caller's outcome, not necessarily the callee's outcome.
Idea in one sentence: Crossing a process boundary buys isolation, but it turns a simple call into a protocol with bytes, buffers, lifecycle, and an outcome that each side may observe differently.
Core Insight
Imagine a document-preview service on one host. The web process receives an uploaded file and asks a separate renderer process to produce a thumbnail. Keeping the renderer separate is intentional: it parses untrusted input, uses a lot of CPU, and may crash. The web process should survive if the renderer does not.
The first implementation looks almost like a local function call:
thumbnail = renderer.make_preview(upload)
That model is comfortable while renderer and caller share one process. The caller can pass an object, the function either returns or raises, and a crash ends both together. It breaks as soon as the renderer is a different process. The web process cannot hand it an in-memory object directly. It must send bytes through an operating-system boundary, decide how long to wait, interpret a closed connection, and determine whether a missing reply means “no work happened” or merely “the reply did not arrive.”
That is the central design shift. IPC is not a smaller spelling of a function call. It is a communication contract between independently scheduled, independently failing processes. The trade-off is clear: stronger isolation contains a renderer failure, but it costs message design, buffering, lifecycle management, and explicit uncertainty.
The Promise We Need to Keep
The preview service needs three promises:
1. A malformed file must not take down the web process.
2. A slow renderer must not make every request wait forever.
3. The web process must know which outcomes it can safely retry.
These promises make a local helper process a reasonable boundary. A Unix-domain socket is one concrete IPC mechanism: Linux documents the AF_UNIX socket family for communication between processes on the same machine. A stream socket is a byte stream, while a datagram or sequenced-packet socket has different message-boundary properties. The choice is part of the protocol; “we use a socket” does not by itself define a request format or completion rule.
In plain English:
A boundary is a place where one part of the system can no longer assume that another part shares its memory, timing, or fate.
In this service:
The web process owns the client connection and upload request. The renderer owns parsing and thumbnail creation. They can communicate, but either process can be delayed, restarted, or stopped independently.
Technical name:
Inter-process communication (IPC) is the operating-system-mediated exchange of data or capabilities between separate processes. Pipes, Unix-domain sockets, shared memory, and passed file descriptors are different IPC designs, not interchangeable details.
The Naive Design: “Write the File and Read One Reply”
Suppose the team chooses a Unix-domain stream socket and invents this request shape:
web -> renderer: bytes of uploaded document
renderer -> web: bytes of thumbnail path
It works for one small document in a quiet test. The renderer reads what arrives, produces an image, and replies. The design hides two questions that the local-call model did not need to ask.
First, where does one request end? A stream socket delivers an ordered stream of bytes, not automatically “one application request per read.” One read may return only part of a header, all of one request plus part of another, or wait because no bytes are available. The protocol needs framing: for example, a fixed-size length header followed by that many payload bytes.
Second, what limits unfinished work? If the web process writes uploads faster than the renderer reads them, a buffer fills. A blocking write can delay the web worker; an unbounded application queue can consume memory; a nonblocking write can report that the caller must wait or reject work. These are not defects in IPC. They are the backpressure decision the boundary forces the design to make.
The naive design works only while messages are tiny, the peer is healthy, and demand stays below capacity. The missing model is that each message has a lifecycle, and each side sees only part of it.
A Better Boundary: Make One Request Inspectable
Use a small explicit protocol. The fields below are a teaching model, not a required wire format.
request
request_id: 91f2
content_length: 4_000_000 bytes
deadline: 800 ms from now
response
request_id: 91f2
outcome: completed | rejected | failed
thumbnail_path: present only when completed
The request_id lets the two processes refer to the same unit of work in logs and retries. A length gives a stream receiver a way to find the payload boundary. A deadline names how long the caller is willing to wait; it does not force the renderer to stop exactly at that instant. The response separates successful completion, explicit refusal, and a handled failure.
This buys us a visible contract, but it costs implementation work. Both processes must agree on framing, maximum sizes, timeouts, cleanup, and versioning. That cost is worthwhile when process isolation protects a real risk. It is unnecessary ceremony for a tiny pure transformation that needs no isolation and shares the caller's fate.
Worked Trace: The Reply Never Arrives
Trace request 91f2. Times are illustrative.
| Time | Web process can observe | Renderer can observe | Request state |
|---|---|---|---|
| 0 ms | upload accepted | nothing yet | created in web process |
| 5 ms | header and payload written | bytes become readable | delivered to kernel buffers |
| 20 ms | waiting for reply | request decoded; render starts | renderer owns processing |
| 300 ms | still waiting | renderer has produced thumbnail | work may be complete |
| 305 ms | connection closes before reply | process crashes during reply | caller lacks a completion record |
| 800 ms | deadline expires; return fallback | renderer is gone | caller outcome is timeout/failure |
At 305 ms, the web process has a hard question. Did the thumbnail get written before the crash? The answer is not available merely because its read failed. The renderer may have finished its output and died before replying, or it may have died first. A closed local socket is a real failure boundary even though no network was crossed.
The safe next action depends on the operation. If thumbnail generation writes to a deterministic temporary path and publishing is atomic, retrying with the same request ID may be safe after checking whether the result already exists. If the helper charges a credit, sends email, or performs a non-idempotent update, retrying blindly could repeat the effect. Idempotency means that repeating a request has the same intended result as doing it once; it is a contract property, not a magical retry setting.
So far, we have seen why “the request timed out” is a statement about the caller. It says the caller did not obtain a usable answer by its deadline. It does not prove that the peer did no work.
What the Operating System Boundary Adds
Separate processes get separate address spaces and lifecycles. That is the isolation benefit. The cost is that data and control must cross a kernel-managed mechanism.
For the renderer, useful IPC choices have different shapes:
| Mechanism | Useful when | Important boundary |
|---|---|---|
| Pipe | One-directional stream between related processes is enough | Byte framing, finite buffer, reader/writer closure |
| Unix-domain socket | A local client/server protocol, bidirectional replies, or peer information is useful | Connection lifecycle, stream framing, permissions or credentials |
| Shared memory | Large data should avoid repeated copying | Ownership, synchronization, and cleanup become explicit |
| File-descriptor passing | The peer should use an already opened resource | It transfers a reference, not a copied file; receiving process still needs a policy for lifetime and access |
Linux AF_UNIX sockets can pass file descriptors and process credentials using ancillary data. This is powerful, but it is not a reason to begin with it. A simple pathname or socket-pair protocol with bounded messages is easier to inspect. Use descriptor passing when the boundary genuinely benefits from sharing an already opened resource or capability.
Do not make the opposite mistake either: “same host” does not mean “no security or lifecycle design.” A pathname Unix socket has filesystem ownership and permission behavior; abstract socket names are Linux-specific and do not use filesystem permissions. The correct choice depends on the trust boundary and portability requirements.
Local Call, IPC, and RPC Are Different Promises
Choose the smallest boundary that provides the protection needed.
| Choice | What the caller gets | What it gives up |
|---|---|---|
| Local function call | Shared memory, simple return/exception path | No process isolation; a crash affects both |
| IPC on one host | Process isolation and independent restart | Bytes, buffers, peer lifecycle, timeout and cleanup rules |
| RPC to another host | Independent placement and scaling | Network delay, remote scheduling, serialization, and wider partial failure |
| Asynchronous queue | Hand-off without waiting for processing now | Backlog, delivery, ordering, duplicate, and completion tracking |
This is not a ladder where each wider boundary is automatically better. A local call is often the best design when the work belongs in one fault domain. IPC is a good fit when a parser, plugin, or privileged helper needs containment on the same host. RPC is appropriate only when the work truly needs a remote boundary. Moving a renderer to another machine does not make it more reliable by itself; it adds new failure states that the contract must handle.
The transfer from IPC to RPC is precise: the local trace already taught the important correction. A client timeout cannot prove a callee did nothing. RPC adds network and remote-host uncertainty to that same gap in knowledge. Protocol choice, service discovery, and distributed retry design are outside this track; the host-level move is to identify the process boundary and observe whether work is blocked, queued, accepted, or completed.
Operational Consequences: Backpressure Is Part of the API
The renderer cannot process infinite concurrent uploads. A sound boundary states what happens when it is full:
bounded local queue
-> accept up to N pending render requests
-> reject or degrade new work when N is full
-> expose queue age and in-flight count
This protects the web process from holding unlimited uploads in memory and makes overload visible early. The trade-off is that some requests receive a fast rejection or a placeholder rather than waiting hopefully. That is often better for a user-facing path than a growing queue whose eventual latency is unknown.
Useful evidence follows the request across the boundary:
- request ID in both processes' logs;
- socket connection, error, and close events;
- accepted, in-flight, completed, and rejected counts;
- queue depth and age, not only average render time;
- renderer restart history and the reason a request ended.
These signals distinguish similar symptoms. A web worker blocked writing points toward a full local path or slow peer. A rising local queue with idle CPUs may mean the renderer waits on storage or a lock. Repeated EPIPE or connection resets point toward peer lifecycle, not an ordinary application exception. The next diagnostic step should follow that evidence rather than assuming “IPC is slow.”
Check Your Understanding
Check: The web process gets EPIPE while sending a preview request. Does that prove the renderer processed no bytes?
Think first, then reveal.
Answer: No. It proves the stream peer was closed for that write. Some earlier bytes may have been received and work may already have begun. Use the request ID and a completion record to decide whether retrying is safe.
Check: A local renderer queue grows while web CPU is low. Should the team first increase web-worker CPU shares?
Think first, then reveal.
Answer: Not from that evidence. Inspect renderer in-flight work, queue age, blocked states, storage access, and process lifecycle. Low web CPU does not say which resource prevents the helper from progressing.
Practice: Review a Boundary Before Adding a Queue
A service sends 30 MiB images to a local conversion helper through a Unix-domain stream socket. During a burst, web workers block in writes, memory rises, and the helper restarts after malformed files. The team proposes “put every image into an unbounded in-memory queue.”
Write a better boundary contract.
A good answer should mention:
- framing and maximum request size for the byte stream;
- a bounded queue or concurrency limit plus a clear rejection, fallback, or admission rule;
- a request ID and a way to distinguish accepted, completed, failed, and unknown outcomes;
- restart handling and whether reprocessing the same image is idempotent;
- evidence such as queue age, blocked writers, helper restart reason, and completion counts.
Resources
- [ARTICLE] unix(7): sockets for local interprocess communication — Focus: Compare stream, datagram, and sequenced-packet behavior; inspect local credentials, pathname permissions, and descriptor passing.
- [ARTICLE] pipe(7): overview of pipes and FIFOs — Focus: See the finite-buffer and reader/writer-closure behavior behind basic pipe IPC.
- [ARTICLE] socketpair(2) — Focus: Inspect how a connected local socket pair creates an unnamed IPC channel.
- [BOOK] Operating Systems: Three Easy Pieces — Focus: Review process isolation and the practical purpose of inter-process communication.
Key Takeaways
- IPC is a process boundary: it exchanges bytes or capabilities between independently scheduled and failing processes.
- A stream needs application framing, and every local boundary needs bounds on unfinished work.
- A timeout or closed connection describes the caller's observed outcome; it does not automatically reveal the callee's completed work.
- Choose IPC for a concrete isolation benefit, then make retries, lifecycle, backpressure, and evidence part of the contract.
- RPC extends the same uncertainty across a network, but its distributed mechanisms should not be confused with host-local IPC.