Processes and Message Passing
The BEAM's lightweight processes -- how to spawn them, why they're nothing like OS threads, and how they communicate.
3 min read
Everything distinctive about Elixir traces back to one thing: the BEAM's model of processes. Not operating system processes, and not threads — something much lighter, and this is where Elixir's reputation for massive concurrency comes from.
Spawning a process
pid = spawn(fn -> IO.puts("Hello from a process!") end)
IO.inspect(pid)
# => #PID<0.123.0>spawn/1 starts a new, completely independent process running the given function, and immediately returns a PID (process identifier) you can use to interact with it. That process has its own stack, its own heap, and its own garbage collector — nothing about it is shared with the process that spawned it.
This is the key difference from OS threads: BEAM processes are managed entirely by the Erlang runtime, not the operating system, which is why a single BEAM node can comfortably run hundreds of thousands to millions of them concurrently. Each one starts at around 2KB of memory — spawning a process is closer in cost to allocating a small object than to spinning up an OS thread.
Processes share nothing
Because processes don't share memory, the only way for them to interact is by sending messages. This is deliberate: no locks, no shared mutable state, no risk of one process's bug corrupting another's data.
send(self(), {:greeting, "hello"})
receive do
{:greeting, message} -> IO.puts("Got: #{message}")
endsend/2 delivers a message to a process's mailbox (here, sending to self/0, the current process); receive blocks until a matching message shows up, using the same pattern-matching rules you've already learned.
Talking to another process
defmodule Pinger do
def loop do
receive do
{:ping, from} ->
send(from, :pong)
loop()
end
end
end
pid = spawn(Pinger, :loop, [])
send(pid, {:ping, self()})
receive do
:pong -> IO.puts("Got pong back!")
endPinger.loop/0 calls itself recursively after handling each message — this recursive-receive pattern is exactly how long-lived, stateful processes work in Elixir, since a process without a message loop simply finishes and terminates after its function returns.
Processes crash independently
Since processes share no memory, a crash in one doesn't corrupt another — it can only exit and, if something is watching, send an :EXIT signal:
pid = spawn(fn -> raise "boom" end)
Process.alive?(pid)
# => false (moments later)The rest of your system carries on completely unaffected. This isolation is precisely what makes the "let it crash" philosophy mentioned earlier in this course viable: a failure is contained to one process, not the whole application.
Why this matters at scale
A web server handling ten thousand concurrent connections can spawn one lightweight BEAM process per connection, each fully isolated, each scheduled fairly by the BEAM's preemptive scheduler so one busy process can't starve the others. That's the architecture underneath Phoenix (covered later in this course) and it's the direct, practical payoff of everything in this lesson: processes and message passing aren't an advanced side-feature of Elixir, they're the foundation everything else — GenServer, Supervisors, and Phoenix itself — is built on top of.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.