Skip to main content

On This Page

The BEAM Runtime: Why Elixir Scales Differently than the JVM

3 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

The BEAM Is Not Like Other Runtimes (And That’s Why Elixir Scales the Way It Does)

The BEAM virtual machine utilizes lightweight processes rather than OS threads to achieve massive concurrency. A single BEAM process starts with only 2KB of stack space, allowing a standard machine to run 100,000 concurrent processes with ease.

Why This Matters

In many runtimes, concurrency is limited by OS thread overhead and stop-the-world garbage collection pauses that create latency spikes. The BEAM eliminates these issues by providing per-process heaps and a preemptive scheduler that allocates a strict budget of 2,000 reductions per turn, ensuring no single task can monopolize CPU resources. This isolation means that failure or heavy garbage collection in one process does not degrade the performance of the rest of the system.

Key Insights

  • BEAM processes start with approximately 2KB of stack space and grow as needed, allowing for millions of concurrent processes.
  • The scheduler uses a reduction-based budget of 2000 units of work per process to prevent tight loops from starving other tasks.
  • Garbage collection is per-process and independent, eliminating global stop-the-world pauses common in the JVM.
  • Data sharing is prohibited; processes communicate via asynchronous message passing which requires copying data into the recipient’s mailbox.
  • Native Implemented Functions (NIFs) bypass the preemptive scheduler and must execute in under 1ms or use dirty schedulers to avoid blocking threads.
  • IO operations are non-blocking at the scheduler level, using system-native polling like epoll or kqueue to manage connection state.

Working Examples

Demonstrating that spawning 100,000 processes is idiomatic and lightweight on the BEAM.

pids = Enum.map(1..100_000, fn i -> spawn(fn -> Process.sleep(:timer.seconds(10)) end) end)

A tight loop consumes reductions and is preempted, allowing other processes to continue execution.

defmodule Spinner do def spin(n) do spin(n + 1) end end; spawn(fn -> Spinner.spin(0) end); spawn(fn -> IO.puts("I still run") end)

A safe receive block with a catch-all pattern to prevent mailbox bloat.

receive do {:ok, value} -> handle(value) other -> Logger.warning("Unexpected message: #{inspect(other)}") end

Practical Applications

  • Phoenix Framework: Successfully handles millions of concurrent WebSocket connections by mapping each connection to a single BEAM process.
  • Fault Tolerance: Implementing the ‘Let it crash’ philosophy where supervisors restart isolated processes in known good states.
  • Pitfall: Sending large data structures between processes can cause performance bottlenecks due to the overhead of memory copying.
  • Pitfall: Executing heavy computation in a NIF without marking it as a dirty job can block the scheduler and cause system-wide latency spikes.

References:

Continue reading

Next article

Solving the Data Layer Problem in Agentic AI Systems

Related Content