TCP vs Unix Sockets vs Named Pipes vs Shared Memory
TCP vs Unix Sockets vs Named Pipes vs Shared Memory
A side-by-side comparison of the four most common IPC mechanisms. None is universally best; choose based on access pattern, performance needs, and platform.
At a glance
| TCP | Unix sockets | Named pipes (FIFO) | Shared memory | |
|---|---|---|---|---|
| Cross-machine | ✅ yes | ❌ no (local only) | ❌ no | ❌ no |
| Cross-OS | ✅ universal | ⚠️ Linux/macOS/BSD; Windows recent | ✅ Unix + Windows (different APIs) | ⚠️ POSIX shm or System V |
| Bidirectional | ✅ | ✅ | ❌ unidirectional (need 2 for both ways) | ✅ (with sync) |
| Stream / message | stream (TCP) or message (UDP) | both | stream | byte buffer |
| Latency | ~15–30µs loopback | ~5–10µs | ~5–10µs | ~50ns–1µs |
| Throughput | ~1 GB/s | ~5–10 GB/s | ~5–10 GB/s | RAM-bound |
| Auth/permissions | none built-in | filesystem perms | filesystem perms | filesystem perms |
| Multiple concurrent clients | ✅ via accept loop | ✅ via accept loop | ❌ need extra plumbing | ⚠️ via reader/writer count |
| Browser-accessible | ✅ if HTTP | ❌ (need bridge) | ❌ | ❌ |
| Complexity | medium | low | low | high (synchronisation) |
TCP
The general-purpose network protocol. Works between any two machines that can route to each other, including loopback (127.0.0.1).
When to use:
- Need to talk across machines, or might in the future.
- Browser is the client (with HTTP layered on top).
- You want maximum tooling —
curl, Wireshark, telnet — for debugging.
When NOT to use:
- Both processes are local and will always be. You're paying the TCP/IP stack cost for no benefit.
For local IPC: TCP loopback is ~3× slower than Unix sockets and offers no security (any local process can connect). If you want HTTP for the developer ergonomics, layer it on Unix sockets via a bridge.
Unix domain sockets
Same socket API as TCP, but the address is a filesystem path (or abstract name on Linux). Communication stays in the kernel.
When to use:
- Local-only IPC, single host.
- Want filesystem-style permissions for access control.
- Performance matters and processes are co-located.
When NOT to use:
- Cross-machine.
- Browsers are clients (use HTTP bridge).
- Windows is a target and you can't require Windows 10 17063+ (Unix sockets there but spotty support).
This is the right default for local daemons, including mxr, lazydap, Docker, dbus, X11, postgres' default config, and most modern Linux/macOS local services.
See Unix Domain Sockets for depth.
Named pipes (FIFOs)
A unidirectional byte stream identified by a filesystem path. mkfifo /tmp/myfifo creates one; one process writes to it, another reads.
When to use:
- Simple producer-consumer pipeline between two known processes.
- You want to use shell redirection (
echo hello > /tmp/myfifo). - Unix-based tools that already understand pipes.
When NOT to use:
- Need bidirectional communication (use Unix sockets, or two pipes).
- Multiple concurrent readers/writers (FIFOs don't multiplex well — readers race).
- Anything modern. Unix sockets do everything FIFOs do plus more.
Windows equivalent: "Named pipes" on Windows are different — bidirectional, multi-instance, more like Unix sockets. Same name, different thing.
Modern code rarely uses POSIX FIFOs. Use Unix sockets or full IPC instead.
Shared memory
Multiple processes map the same memory region. Reads and writes touch the same physical pages.
When to use:
- Maximum throughput. RAM-bound.
- Large data transfers between local processes (gigabytes).
- High-frequency, low-latency: video frames, market data, real-time audio buffers.
When NOT to use:
- You don't have a concrete need to escape Unix-socket performance (5 GB/s is plenty for almost anything).
- You're not prepared to manage explicit synchronisation (semaphores, atomics, lock-free data structures).
- The processes might crash mid-update — no "transaction" semantics; partial writes are visible.
Implementation: POSIX shm_open + mmap is the modern way. System V shmget is older, more cumbersome, still works.
For mxr / lazydap and most local daemons, shared memory is overkill. The IPC volume is small; messages-per-second are in the hundreds at most. Unix sockets have plenty of headroom.
Choosing for new projects
Decision tree:
- Cross-machine? → TCP, layer your protocol on top.
- Same machine, multiple clients, browser too? → Unix socket + HTTP bridge.
- Same machine, multiple clients, no browser? → Unix socket + your own protocol.
- Same machine, two known processes, simple pipeline? → Named pipe (or stdio if parent-child).
- Same machine, ridiculous throughput needed? → Shared memory + ring buffer.
- Different processes spawned by one parent, byte stream is fine? → anonymous pipes (
pipe(2)), pass to child via fork.
For 90% of "I'm building a local daemon" cases, the answer is "Unix socket." mxr, lazydap, Docker, postgres, sshd-agent, cups, dbus all chose this. Don't overthink.
What mxr and lazydap chose, and why
Both: Unix domain sockets, stream type, length-prefixed JSON framing.
Reasoning:
- Local daemons, never cross-machine.
- Multiple concurrent clients (CLI, TUI, agent skill).
- Filesystem permissions give clean per-user isolation (
0700mode). - Fast enough; the per-message overhead is negligible compared to the actual work (parsing email, fetching DAP messages from an adapter).
- Cross-language friendly (any language can implement the framing + JSON).
- Easy to debug (
socatopens the socket and you can read raw JSON). - Easy to wrap in HTTP via a bridge if a future browser client needs it.
The choice that most local daemons make. Worth knowing the alternatives, but the default is right.
See also
- Unix Domain Sockets — the chosen winner, with depth
- Local IPC vs HTTP — when to leave the local layer
- Length-Prefixed Framing — the framing on top
- Inter-Process Communication (IPC) — broader menu
- How Processes Talk to Each Other — synthesis