The BEAM Explained
Understanding what is BEAM and how we can use the BEAM philosophy to make better orchestration for agents
What is BEAM?
BEAM = Bogdan/Björn's Erlang Abstract Machine.
It's the virtual machine/runtime used primarily by:
- Erlang/OTP
- Elixir
- Gleam
- LFE
- other BEAM languages
The important mental model is:
Your Elixir code
↓
Elixir compiler
↓
BEAM bytecode
↓
BEAM VM
↓
Operating System
↓
CPU
It's a register-based bytecode VM (its predecessor JAM was stack-based). But the bytecode interpreter is the least interesting part. What matters is the process model, the scheduler, and the memory architecture.
The high-level architecture
A simplified BEAM architecture looks like this:
┌──────────────────────────┐
│ Elixir / Erlang │
│ Applications │
└────────────┬─────────────┘
│
▼
┌──────────────────────────┐
│ BEAM Bytecode │
└────────────┬─────────────┘
│
┌───────────────┴────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Scheduler │ │ Process Runtime │
└────────┬────────┘ └────────┬────────┘
│ │
┌────────┴────────┐ ┌────────┴─────────┐
│ │ │ │
▼ ▼ ▼ ▼
Scheduler 1 Scheduler N Heap/Stack Mailbox
│ │
└────────┬────────┘
▼
┌───────────────────┐
│ Native OS Threads │
└───────────────────┘
│
▼
CPU
And around all of this you have:
┌────────────────────────────────────────────────┐
│ BEAM VM │
│ │
│ │
│ Scheduler ─── Processes ─── Mailboxes ─── GC │
│ │ │ │ │
│ │ │ │ │
│ └─────────────┴─────────────┘ │
│ │ │
│ Supervision / OTP │
│ │ │
│ Distribution / Networking │
│ │ │
│ Ports / NIFs / OS │
└────────────────────────────────────────────────┘
BEAM is a process-oriented VM
This is probably the biggest difference between BEAM and something like Node.js, Python, or the JVM.
When you write:
spawn(fn ->
IO.puts("Hello")
end)
BEAM creates a BEAM process.
This is NOT an operating-system process.
It's also not an OS thread.
A BEAM process is an extremely lightweight unit of execution managed by the VM.
You can have:
Application
│
├── Process 1
├── Process 2
├── Process 3
├── Process 4
├── ...
└── Process 1,000,000
Potentially millions of processes can exist.
BEAM processes vs OS processes
Traditional architecture:
Application
│
├── OS Process
│ └── Thread
│
├── OS Process
│ └── Thread
│
└── OS Process
└── Thread
BEAM:
BEAM VM
│
├── BEAM Process
├── BEAM Process
├── BEAM Process
├── BEAM Process
├── ...
└── millions of BEAM Processes
BEAM processes are:
- lightweight
- isolated
- independently scheduled
- independently garbage collected
- communicating through messages
This is why Elixir code can comfortably model things as independent actors.
The Actor Model
BEAM's architecture is heavily based around the Actor Model.
Think of every process as an actor:
┌──────────────┐
│ Process A │
│ │
│ State │
│ Mailbox │
└──────┬───────┘
│
message
│
▼
┌──────────────┐
│ Process B │
│ │
│ State │
│ Mailbox │
└──────────────┘
A process:
- Has its own state
- Receives messages
- Processes messages
- Changes its state
- Sends messages to other processes
For example:
def loop(count) do
receive do
:increment ->
loop(count + 1)
{:get, caller} ->
send(caller, {:count, count})
loop(count)
end
end
This process owns:
count
Other processes don't directly modify it.
They communicate through messages.
Mailboxes
Every BEAM process has a mailbox.
Process A
│
│ send(:hello)
▼
┌──────────────────────┐
│ Process B │
│ │
│ Mailbox: │
│ ┌──────────────────┐ │
│ │ :hello │ │
│ │ :increment │ │
│ │ {:foo, 123} │ │
│ └──────────────────┘ │
│ │
│ State: │
│ count = 42 │
└──────────────────────┘
In Elixir:
send(pid, :hello)
The sender doesn't directly call a function inside the receiver.
It sends a message.
The receiver:
receive do
:hello ->
...
end
This is fundamental to BEAM, But this is powerful ?
Suppose you build a web application.
Instead of having:
HTTP Request
↓
Controller
↓
Database
↓
Response
you can have:
┌── User Process
│
Request → Router → ├── Chat Process
│
├── Notification Process
│
└── Payment Process
Each component can have its own process/state.
This becomes especially powerful for:
- chat systems
- multiplayer games
- messaging
- real-time applications
- distributed systems
- queues
- IoT
- financial systems
- AI agents
Schedulers
You might have:
1,000,000 BEAM processes
but your machine might only have:
12 CPU cores
How does BEAM execute them? Schedulers.
Conceptually:
CPU cores
│
├── Core 1 ← Scheduler 1
├── Core 2 ← Scheduler 2
├── Core 3 ← Scheduler 3
├── ...
└── Core 12 ← Scheduler 12
Each scheduler executes BEAM processes.
For example:
Scheduler 1
├── P1
├── P17
├── P92
└── P101
Scheduler 2
├── P2
├── P43
└── P76
Scheduler 3
├── P3
├── P8
└── P99
The VM constantly switches between runnable processes.
Preemptive scheduling
This is another important BEAM feature.
Imagine Process A runs:
loop_forever()
You don't want it to completely destroy the rest of your application.
BEAM uses preemptive scheduling based on reductions.
Roughly:
Process A
↓
executes some work
↓
reduction budget exhausted
↓
scheduler pauses A
↓
Process B runs
↓
Process C runs
↓
Process A gets another turn
So:
A → B → C → D → A → B → C ...
The exact scheduling behavior is more sophisticated than this, but this is the right mental model.
This is one reason BEAM systems remain responsive even with lots of concurrent work.
Reductions
BEAM doesn't simply say:
"Run this process for exactly 5 milliseconds."
Instead, it uses an execution accounting mechanism called reductions.
A reduction roughly represents a unit of BEAM work.
Conceptually:
Process A
Reduction 1
Reduction 2
Reduction 3
...
Reduction N
↓
Scheduler switches process
This prevents one process from monopolizing a scheduler.
Each process has its own memory
This is another huge architectural difference.
Consider:
a = spawn(...)
b = spawn(...)
Conceptually:
BEAM
│
├── Process A
│ ├── Heap
│ ├── Stack
│ └── State
│
└── Process B
├── Heap
├── Stack
└── State
Processes don't normally share mutable memory.
Instead:
Process A
│
│ message
▼
Process B
This greatly reduces problems involving:
- locks
- race conditions
- shared mutable state
- deadlocks
Garbage collection
Because each BEAM process has its own heap, garbage collection is also largely per-process.
Imagine:
Process A
Heap = 10 MB
Process B
Heap = 50 KB
Process C
Heap = 100 MB
If Process B needs garbage collection:
GC Process B
doesn't mean:
GC entire application
This can be a major advantage for highly concurrent systems.
Immutable data
Elixir uses immutable data.
For example:
user = %{name: "Manish"}
user2 = %{user | name: "John"}
"user" wasn't modified.
You created another value.
user
↓
{name: "Manish"}
user2
↓
{name: "John"}
This works extremely well with BEAM's process model.
Instead of:
Shared mutable state
↓
locks
↓
race conditions
you get:
Process A
│
immutable state
│
message
▼
Process B
│
immutable state
Fault isolation
This is where BEAM starts becoming really special.
Suppose you have:
Web Server
│
├── User process
├── Chat process
├── Payment process
├── Notification process
└── AI process
Suppose the AI process crashes.
In a normal application, you might get:
AI crashes
↓
Exception
↓
maybe application crashes
BEAM's philosophy is different:
Let it crash.
A process can crash without taking down unrelated processes.
AI Process
❌
User Process
✓
Chat Process
✓
Payment Process
✓
But how do we recover?
That leads to OTP.
OTP and Supervisors
OTP stands for Open Telecom Platform. Despite the name, it isn't really about telecommunications anymore.
OTP provides abstractions for building reliable systems.
One of the most important concepts is the Supervisor.
Imagine:
Supervisor
│
├── Worker A
├── Worker B
├── Worker C
└── Worker D
If Worker C crashes:
Supervisor
│
├── Worker A ✓
├── Worker B ✓
├── Worker C ❌
└── Worker D ✓
The supervisor can restart C:
Supervisor
│
├── Worker A ✓
├── Worker B ✓
├── Worker C 🔄
└── Worker D ✓
This is called supervision.
Supervision trees
Real applications form trees.
For example:
Application Supervisor
│
├── Web Supervisor
│ ├── Endpoint
│ └── Connection Supervisor
│
├── Database Supervisor
│ ├── DB Worker
│ └── Pool
│
├── AI Supervisor
│ ├── Agent 1
│ ├── Agent 2
│ └── Agent 3
│
└── Notification Supervisor
├── Email Worker
└── Push Worker
If something fails:
Worker crashes
↓
Parent supervisor
↓
restart worker
If the entire subsystem fails:
Subsystem crashes
↓
Higher supervisor
↓
restart subsystem
This creates a hierarchy of fault recovery.
"Let it crash"
This phrase is frequently misunderstood.
It doesn't mean:
Write terrible code and ignore errors.
It means:
Don't make every component responsible for recovering every possible failure.
Instead:
Worker
↓
does its job
↓
unexpected failure
↓
crashes
↓
Supervisor detects it
↓
restarts it
This dramatically simplifies certain kinds of resilient systems.
Distribution
BEAM wasn't designed only for one machine.
It was designed for distributed systems.
Imagine:
Machine A
┌───────────────────┐
│ BEAM │
│ │
│ Process A │
│ Process B │
└─────────┬─────────┘
│
network
│
▼
Machine B
┌───────────────────┐
│ BEAM │
│ │
│ Process C │
│ Process D │
└───────────────────┘
Processes can communicate across machines.
Conceptually:
send(remote_process, {:hello, "world"})
The BEAM distribution system handles the networking underneath.
Nodes
A running BEAM VM can be a node.
For example:
Node A
server@machine1
Node B
server@machine2
Node C
server@machine3
They can form a distributed BEAM cluster:
Cluster
│
┌────────┼────────┐
│ │ │
Node A Node B Node C
│ │ │
P1 P2 P3 P4 P5 P6
Processes can communicate between nodes.
This is one of the reasons Erlang/Elixir became famous for telecom systems.
Hot code upgrades
Another unusual BEAM feature is the ability to replace code while a system is running.
Conceptually:
Version 1
↓
Application running
↓
Deploy Version 2
↓
Some processes use V1
Some processes transition to V2
↓
V2 becomes active
This was historically extremely important for systems that couldn't simply shut down.
Modern deployment practices often use other strategies, so you shouldn't assume every Elixir production deployment relies on hot upgrades—but the runtime supports sophisticated code replacement.
Ports and NIFs
BEAM doesn't live in isolation.
Sometimes you need to interact with:
- OS processes
- C/C++
- Rust
- external programs
- files
- hardware
BEAM provides mechanisms such as:
Ports
BEAM
│
│ Port
▼
External OS process
NIFs Native Implemented Functions allow native code to run from BEAM.
Elixir
↓
NIF
↓
Rust/C/C++
But NIFs need care because poorly behaved native code can interfere with VM responsiveness.
I/O and asynchronous work
BEAM is designed to handle enormous amounts of concurrent I/O.
For example:
10,000 connections
│
▼
BEAM VM
│
┌─────┼─────┐
▼ ▼ ▼
P1 P2 P3 ...
Most connections spend much of their time waiting:
waiting for:
- network
- database
- socket
- message
The scheduler can execute another process while one is waiting.
That's where BEAM shines.
BEAM + Phoenix
This is where you can see the architecture in a real application.
A Phoenix application might look conceptually like:
Internet
│
▼
Phoenix Endpoint
│
┌─────┴─────┐
│ │
Request WebSocket
│ │
▼ ▼
Process Process
│ │
└─────┬─────┘
│
Application
│
┌────────────┼────────────┐
▼ ▼ ▼
Database Cache Workers
Thousands of concurrent connections can be represented as lightweight BEAM processes.
That's one reason Phoenix is particularly strong for real-time applications.
AI agents
Imagine you're building an AI coding agent.
Traditional architecture:
Python Agent
│
├── LLM
├── Git
├── GitHub
├── Jira
├── Slack
└── Database
With BEAM you can model each agent/component as a process:
Agent Supervisor
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
Planner Researcher Coder
Process Process Process
│ │ │
▼ ▼ ▼
Tools Tools Tools
│ │ │
└─────────────┼─────────────┘
▼
Reviewer
Process
Now imagine 100 agents:
Agent Supervisor
│
├── Agent 1
├── Agent 2
├── Agent 3
├── ...
└── Agent 100
Each agent can maintain its own state.
AI agents map surprisingly well to BEAM processes
An agent naturally has:
State
- Mailbox
- Decision loop
- Tools
- Failures
That's almost exactly a BEAM process.
For example:
Agent Process
│
├── State
│ ├── task
│ ├── conversation
│ ├── plan
│ └── tool results
│
├── Mailbox
│ ├── user message
│ ├── tool result
│ └── agent message
│
└── Actions
├── call LLM
├── call GitHub
├── call Jira
└── call database
And if the agent crashes:
Agent
❌
↓
Supervisor
↓
restart
That's a very natural fit.
Does it replace python?
You generally wouldn't say:
"Python code is going to be written in Elixir."
Instead:
Elixir / BEAM
│
Agent orchestration
│
┌──────────┼──────────┐
▼ ▼ ▼
LLM API GitHub Jira
│
▼
Python service
│
▼
ML / data processing
You can have:
Elixir
↓
Agent runtime
↓
Python service
↓
ML model
or:
Elixir
↓
LLM API
depending on the task.
Elixir's advantage is primarily the runtime architecture and concurrency model, not replacing Python's ML ecosystem.
BEAM vs Node.js
A useful comparison:
Node.js
Event loop
│
├── callback
├── callback
├── callback
└── callback
BEAM
Schedulers
│
├── Process
├── Process
├── Process
├── Process
└── Process
Node.js also supports worker threads/processes, so this isn't a claim that Node can't do concurrency.
The architectural philosophy is different.
BEAM makes isolated concurrent processes a first-class primitive.
BEAM vs Python
Python:
Application
│
├── Thread
├── Thread
├── Thread
└── Process
Concurrency often involves:
- asyncio
- threads
- multiprocessing
- queues
- external workers
BEAM:
Application
│
├── Process
├── Process
├── Process
├── Process
└── Process
The runtime itself is designed around this model.
Summary
Lightweight Processes
│
▼
Message Passing
│
▼
Isolated State
│
▼
Preemptive Scheduling
│
▼
Fault Isolation
│
▼
Supervisors
│
▼
Automatic Recovery
│
▼
Distribution
│
▼
Highly Available Systems
That's the core philosophy.
that's why Elixir + BEAM becomes particularly interesting for the agent systems you've been exploring.
Instead of thinking:
"How do I make my Python agent call 10 tools concurrently?"
you start thinking:
"What should be a process? What state should it own? What messages should it receive? What happens if it crashes? Who supervises it?"
That's a much more powerful architectural mindset for long-running, concurrent AI systems.
If you're going down this path, the next concepts I'd learn in order are:
BEAM processes → mailboxes → schedulers/reductions → GenServer → Supervisor → supervision trees → Registry → DynamicSupervisor → OTP applications → distributed BEAM → Phoenix/LiveView.