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:

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

What this enables

A daemon that speaks this stack can be consumed by:

This is the basis of Headless Core + Multiple Clients architectures generally and The Local Daemon Pattern specifically.

$ mxr search "from:alice is:unread" --format json

What happens:

  1. Client (mxr binary, search subcommand): builds an IpcMessage { id: 1, payload: Request::Search { query: "from:alice is:unread", limit: ... } }.
  2. Client connects to ~/.cache/mxr/mxr.sock. (If daemon not running, auto-spawns it first; see The Local Daemon Pattern.)
  3. Client serialises message via serde_json::to_vec → bytes.
  4. Client writes 4-byte length prefix (big-endian) + bytes → Unix socket.
  5. Kernel buffers; daemon's accept loop has accept()'d a stream for this client.
  6. Daemon reads 4-byte length, then reads exactly N bytes of body.
  7. Daemon parses body via serde_json::from_sliceIpcMessage.
  8. Daemon dispatches by Request variant to the search handler.
  9. Search handler queries Tantivy → list of message IDs → fetches envelopes from SQLite → builds response.
  10. Daemon serialises IpcMessage { id: 1, payload: Response::SearchResults { ... } } and writes back through the same socket.
  11. 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

For everything else (local daemon, multiple clients, modest throughput, cross-language consumers wanted), this stack is the default.

See also (atomic notes)

How this enables the architectures I care about

Other syntheses (sibling MOCs in the vault)

The other "How X actually works" synthesis notes — each is the entry point to its own cluster:

Further reading