Message Passing
"Don't communicate by sharing memory; share memory by communicating." Independent processes exchanging messages over channels, from Go and actors to queues and WebSockets.
There are two broad ways for concurrent things to cooperate. They can share memory and protect it with locks, or they can send each other messages and keep their state private. The Go proverb puts it best: don’t communicate by sharing memory; share memory by communicating.
Why messages
Shared memory with locks is fast and familiar, but it invites race conditions, deadlocks and code where correctness depends on every caller remembering the rules. With message passing, each process owns its state outright. The only way to affect it is to send it a message, which makes the interactions explicit — and much easier to reason about, test and distribute across machines.
Design choices for a channel
- Synchronous or asynchronous. A synchronous send blocks until the receiver takes the message (a rendezvous). An asynchronous one drops it into a buffer and carries on — faster, but you now need to think about full buffers and backpressure.
- Uni- or bi-directional, one-to-one or one-to-many.
- Ordering and delivery guarantees. Within one machine these are easy. Across a network they’re the whole game — see Message Delivery Semantics.
Where it shows up
- Go goroutines and channels.
- Actors (Erlang/Elixir, Akka): every actor has a mailbox and processes one message at a time. Supervisors restart actors that crash — “let it crash” as a reliability strategy.
- Session types go a step further and check at compile time that both sides follow the agreed protocol. I met these at university and still find them a beautiful idea.
- JavaScript itself: the event loop,
postMessageto web workers, and async/await are all cooperative message handling. - Distributed systems: queues, event streams and real-time channels such as WebSockets and Server-Sent Events (SSEs) are message passing between machines.
A small example
Two web workers crunching data for a dashboard could share a buffer guarded by locks (SharedArrayBuffer with Atomics) —
fiddly and easy to get wrong. Or each worker can own its slice and post results back to the main thread, which merges them.
The second version is slower by a copy and simpler by a mile.
In practice
Real-time UIs are message-passing systems wearing a costume. A socket delivers messages; the UI’s job is to apply them to a state it owns, in order, without letting every message trigger a re-render. Thinking of the frontend as “a process with a mailbox” is what led me to the single-owner pattern in Zustand Transient Updates.
- #concurrency
- #message-passing
- #actors
- #channels
↩ Linked from (4)
- Message Delivery SemanticsEvergreen
…there is. The wider picture of what can go wrong is in Types of Failure, and the underlying channel ideas in Message Passing.
- SynchronizationSeed
Related: Threads and Processes · Deadlock and Starvation · Message Passing
…ondition). There are two ways out. You can guard shared state with locks, or avoid sharing altogether and use Message Passing.
- WebSocketsEvergreen
…intermediate values nobody can see, and measure render cost alongside network latency. Underneath, it's still Message Passing: independent parties exchanging messages instead of sharing state.
↪ Links to (4)
Related by topic
- Growingmetaphor
Balance Is a Control Loop
Calisthenics Lab - Growing
Heartbeat
Distributed systems - Seedmetaphor
Regress, Don't Grind: Graceful Degradation
Calisthenics Lab - Growing
AJAX Polling
Real-time UIs