# Message Passing vs Shared Memory

The dominant framing of concurrency for the last several decades has split into two camps: shared-memory coordination (locks, mutexes, semaphores, atomics) and message-passing coordination (channels, mailboxes, actors). The pitch for message passing is that it eliminates the foot-guns of shared state by isolating workers and letting them only exchange values.

Edward Lee challenged the framing in 2006 in *The Problem with Threads*. His argument: both approaches model concurrency as threads of execution that need to be coordinated, so changing the coordination mechanism from locks to messages doesn't change the underlying model — it changes the syntax of failure. Both inherit nondeterminism. Both require the programmer to prune nondeterminism rather than express computation.

## What message passing actually eliminates

Message passing does eliminate one important class of bug: unsynchronized memory access. If two workers only ever communicate by sending values, they cannot simultaneously mutate the same variable. Data races in the strict sense go away.

What it does not eliminate:

- **Deadlocks** — circular waits expressed through queues instead of locks.
- **Leaks** — blocked sender or receiver pinned to a queue nobody else reads.
- **Nondeterministic scheduling** — when multiple workers can receive from the same queue, the runtime picks one.
- **Protocol violations** — wrong message order, sending after close, closing twice.

These categories all map cleanly to classic shared-state bugs. See [[go-channel-bug-patterns]] for the four-way mapping with examples from real Go projects.

## Why the dichotomy is false

The argument hinges on what the message-passing primitive actually is. A real channel — like the kind in pi-calculus or proper functional channel libraries — has two distinct endpoints (producer and consumer) with different types and capabilities. The runtime can detect when one end is gone and clean up the other.

Most production message-passing primitives don't have this. A Go channel is a single concurrent-queue object that any goroutine holding a reference can send to or receive from. A Java `BlockingQueue` is structurally identical and lives in `java.util.concurrent` next to `Mutex` and `Semaphore`. Both are shared mutable data structures used for coordination. The vocabulary differs but the substrate is the same.

Even Erlang, the canonical message-passing language with isolated heaps and copied messages, ships ETS tables: shared mutable storage added because the pure actor model couldn't meet performance requirements. Researchers found races in ETS in the Erlang standard library itself.

## What this implies

The bug study by Tu et al. (2019) is the empirical confirmation: 58% of blocking bugs in Docker/Kubernetes/etcd/gRPC/CockroachDB were caused by message passing, not shared memory. See [[message-passing-shared-mutable-state]] for the full breakdown.

If both coordination mechanisms fail for the same structural reason — the coordination object itself is shared mutable state — then the choice between them is style, not safety. The deeper question is whether either approach is the right foundation at all, which is the open question the author of [[causality-blog]] sets up.

## Related

- [[go-channel-bug-patterns]] — the four classic failure modes that channels reproduce
- [[art-of-multiprocessor-programming]] — Herlihy and Shavit's textbook is where to go next on the shared-memory half: linearizability, lock-free structures, and the consensus hierarchy that ranks synchronization primitives by what they can build
- [[distributed-consensus]] — the impossibility-result lens on coordinating multiple workers
- [[flp-impossibility]] — why no algorithm guarantees consensus in async systems with crashes
