Erlang
Interprocess Communication
Concurrency
Distributed Systems
Programming Languages

Same Machine Erlang communication

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

On a single Erlang node, processes communicate by sending messages directly to process identifiers, or to registered names. The model is the same one Erlang uses across distributed systems, but same-machine communication is simpler because there is no network hop, no external serialization concern in your code, and no need to think about remote node connectivity.

Local Erlang Processes Use Message Passing

Every Erlang process has a mailbox. Another process can send a message with the ! operator, and the receiving process handles it with receive.

Here is a minimal request-reply example:

erlang
1-module(local_demo).
2-export([start/0, worker/0]).
3
4worker() ->
5    receive
6        {From, ping} ->
7            From ! {self(), pong},
8            worker();
9        stop ->
10            ok
11    end.
12
13start() ->
14    Pid = spawn(?MODULE, worker, []),
15    Pid ! {self(), ping},
16    receive
17        {Pid, pong} ->
18            io:format("received pong~n")
19    end,
20    Pid ! stop.

This code works entirely on the same machine inside one Erlang node. The sending side does not call the worker directly. It sends a message and waits for a response.

Registered Names Make Local Services Easier to Reach

If a process acts like a local service, you can register it under a name so callers do not need to know its Pid.

erlang
1-module(counter_server).
2-export([start/0, loop/1, increment/0, get/0]).
3
4start() ->
5    register(counter, spawn(?MODULE, loop, [0])).
6
7loop(Value) ->
8    receive
9        {From, increment} ->
10            NewValue = Value + 1,
11            From ! {ok, NewValue},
12            loop(NewValue);
13        {From, get} ->
14            From ! {ok, Value},
15            loop(Value)
16    end.
17
18increment() ->
19    counter ! {self(), increment},
20    receive
21        Reply -> Reply
22    end.
23
24get() ->
25    counter ! {self(), get},
26    receive
27        Reply -> Reply
28    end.

That pattern is common in small systems and experiments. In larger OTP applications, you would usually wrap the behavior in a gen_server.

Same-Machine Does Not Mean Shared Memory

A key Erlang idea is that processes do not communicate through shared mutable state. Even on the same machine, each process owns its own memory and interacts only by message passing.

That has two important consequences:

  • processes stay isolated from one another
  • concurrency bugs are more often about protocol design than shared-memory races

In practice, you spend more time thinking about message shapes, timeouts, and supervision than about locks.

Add Timeouts and Failure Awareness

Local communication is fast, but a local process can still hang or crash. A request that uses receive without a timeout can block forever.

erlang
1request(Pid, Message) ->
2    Pid ! {self(), Message},
3    receive
4        Reply -> Reply
5    after 1000 ->
6        timeout
7    end.

For stronger failure handling, use monitors:

erlang
1Ref = erlang:monitor(process, Pid),
2Pid ! {self(), ping},
3receive
4    {Pid, pong} ->
5        erlang:demonitor(Ref, [flush]),
6        ok;
7    {'DOWN', Ref, process, Pid, Reason} ->
8        {error, Reason}
9end.

That is still same-machine communication, but now the caller can detect whether the callee died before replying.

When to Move to OTP Behaviours

Raw spawn, !, and receive are excellent for learning and for small focused tasks. Once the process becomes long-lived or central to the system, OTP behaviours such as gen_server give you:

  • standard call and cast patterns
  • supervision support
  • consistent lifecycle handling
  • better operational structure

The communication mechanism is still message passing; OTP just standardizes the protocol.

Common Pitfalls

The most common mistake is forgetting that message receipt order is only guaranteed per sender. If many processes talk to one mailbox, the receiving logic still needs to match messages carefully.

Another pitfall is writing receive blocks that are too broad, which can accidentally consume the wrong message and leave the intended caller waiting.

It is also easy to skip timeouts because the code is local and "should be fast." Local processes still fail, block, or become overloaded.

Finally, avoid building large ad hoc local services around bare processes when OTP already gives you safer building blocks.

Summary

  • Same-machine Erlang communication uses normal process message passing.
  • Send to a Pid or a registered name with the ! operator.
  • Use receive carefully, with timeouts when a reply might not arrive.
  • Keep in mind that local Erlang processes still do not share mutable memory.
  • Move long-lived services toward OTP behaviours such as gen_server.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.