How Processes Talk to Each Other
How Processes Talk to Each Other
Synthesis. The full path from "process A wants to send data to process B" to "process B has the message, parsed, and acted on it." Stitches together the kernel-level mechanisms (Inter-Process Communication (IPC)), the local-IPC choice (Unix Domain Sockets), the byte-stream framing (Length-Prefixed Framing), and the application-level protocol (JSON-RPC or similar).
The full picture
Process A Process B
───────── ─────────
1. Build a message 5. recv() bytes from socket
(e.g., serde_json::to_vec) (kernel hands them up)
│ │
│ ▼
▼ 6. Read 4-byte length prefix
2. Frame the message decode N as message size
(4-byte BE length + body) │
│ ▼
│ 7. Read exactly N bytes
▼ of message body
3. send() bytes to socket │
(kernel buffers them) ▼
│ 8. Parse the body
│ (e.g., serde_json::from_slice)
▼ │
4. Bytes traverse │
the kernel boundary ▼
(Unix socket: stays in kernel; 9. Dispatch to handler
TCP: through TCP/IP stack) (route by request method)
│ │
└───────────────► socket buffer ────────────►──┘
│
▼
10. Build response,
same path back
Four layers, each with its own concerns:
- Layer 1 — Transport (comparative). Bytes from A's process to B's. Almost always Unix sockets for local IPC.
- Layer 2 — Framing (Length-Prefixed Framing). Where one message ends and the next begins.
- Layer 3 — Encoding. JSON, protobuf, msgpack, custom binary. JSON wins for debuggability + cross-language ease.
- Layer 4 — Protocol (JSON-RPC or analogue). What the messages mean. Methods, params, responses, errors, events, request/response correlation.
Each layer has its own failure modes and its own diagnostic tools.
Decisions per layer
For mxr / lazydap and most local daemons:
| Layer | Choice | Why |
|---|---|---|
| Transport | Unix Domain Sockets | Local-only, fast, filesystem permissions, multi-client |
| Framing | Length-Prefixed Framing (4-byte BE) | Simplest reliable framing for streams |
| Encoding | JSON (serde_json) | Cross-language, debuggable, fast enough |
| Protocol | Custom envelope (JSON-RPC-shaped) | Tight fit for use case; bridges to JSON-RPC easily |
Each choice is replaceable independently of the others. You could swap JSON for protobuf without touching the transport. You could swap Unix socket for TCP without touching the protocol.
Why this combination wins for local daemons
- Performance is enough. Unix socket + JSON gives ~100K req/s, far above what mxr or lazydap need.
- Cross-language friendly. A Python script, a Rust client, a Go MCP wrapper — all can implement the framing and JSON parsing trivially.
- Debuggable.
socat UNIX-CONNECT:/path/sock STDIOand you can read traffic by hand. - Secure. Filesystem permissions confine access to the user.
- Flexible. Move to HTTP via a bridge process if browsers need to consume it later. Move to TCP if remote access is needed. Both are additive, not replacements.
What this enables
A daemon that speaks this stack can be consumed by:
- A native CLI (the canonical client)
- A TUI on the same machine
- An agent skill (Claude Code, Cursor, custom)
- A web app via an HTTP bridge
- Any language with a JSON parser
- Shell scripts via
socatif you really want - Future-you, in 2 years, building something nobody anticipated
This is the basis of Headless Core + Multiple Clients architectures generally and The Local Daemon Pattern specifically.
Concrete example: an mxr search
$ mxr search "from:alice is:unread" --format json
What happens:
- Client (
mxrbinary,searchsubcommand): builds anIpcMessage { id: 1, payload: Request::Search { query: "from:alice is:unread", limit: ... } }. - Client connects to
~/.cache/mxr/mxr.sock. (If daemon not running, auto-spawns it first; see The Local Daemon Pattern.) - Client serialises message via
serde_json::to_vec→ bytes. - Client writes 4-byte length prefix (big-endian) + bytes → Unix socket.
- Kernel buffers; daemon's accept loop has
accept()'d a stream for this client. - Daemon reads 4-byte length, then reads exactly N bytes of body.
- Daemon parses body via
serde_json::from_slice→IpcMessage. - Daemon dispatches by
Requestvariant to the search handler. - Search handler queries Tantivy → list of message IDs → fetches envelopes from SQLite → builds response.
- Daemon serialises
IpcMessage { id: 1, payload: Response::SearchResults { ... } }and writes back through the same socket. - Client reads length + body, parses, formats per
--format json, prints to stdout, exits.
End-to-end ~5–20ms, of which ~5ms is network/IPC overhead and the rest is the actual search work. The IPC overhead disappears into the noise.
When this stack is wrong
- Cross-machine → use TCP, possibly HTTP/gRPC.
- High-frequency, low-latency (video frames, market data) → shared memory.
- Browser is a primary client → bridge to HTTP/WebSocket.
- Single-purpose tool with no daemon → just be a one-shot CLI; no IPC needed.
For everything else (local daemon, multiple clients, modest throughput, cross-language consumers wanted), this stack is the default.
See also (atomic notes)
- Inter-Process Communication (IPC) — the menu of options
- Unix Domain Sockets — the transport
- Length-Prefixed Framing — the framing
- JSON-RPC — the protocol shape
- Local IPC vs HTTP — when to leave the local layer
- TCP vs Unix Sockets vs Named Pipes vs Shared Memory — the comparative
How this enables the architectures I care about
- The Local Daemon Pattern — auto-spawning daemons depend on this stack
- Headless Core + Multiple Clients — multi-client architectures depend on a stable wire protocol
- Mxr · Lazydap — concrete instances using exactly this stack
Other syntheses (sibling MOCs in the vault)
The other "How X actually works" synthesis notes — each is the entry point to its own cluster:
- How Debuggers Actually Work — DAP / adapters / ptrace / DWARF (DAP is JSON-RPC over the IPC stack described here)
- How Email Actually Works — SMTP / IMAP / MIME / threading / OAuth (mxr's IPC also uses this stack)
- How Daemons Work — daemon lifecycle (daemons typically listen on the IPC mechanisms described here)
- The Elm Architecture (TEA) — Model / Update / View / Cmd / Reducers
- Client-Agnostic Cores — headless core + many clients, the architectural payoff of having a stable IPC
Further reading
- W. Richard Stevens, Advanced Programming in the UNIX Environment, Chapter 17 (IPC).
- Unix Network Programming, Volume 2: Interprocess Communications (Stevens) — the canonical reference.
man 7 unix— the canonical reference.- LSP / DAP / MCP specs — examples of this stack in production.