જ➣Back to writings

The BEAM Explained

Understanding what is BEAM and how we can use the BEAM philosophy to make better orchestration for agents

Aug 30, 2026·30 min read
⋆⊱༻𖥸༺⊰⋆

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:

  1. Has its own state
  2. Receives messages
  3. Processes messages
  4. Changes its state
  5. 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.