惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Last Week in AI
Last Week in AI
大猫的无限游戏
大猫的无限游戏
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
爱范儿
爱范儿
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
博客园 - 叶小钗
博客园_首页
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
V
V2EX
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
量子位
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
美团技术团队

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
The BEAM Is Not Like Other Runtimes (And That's Why Elixi...
shayan holak · 2026-04-28 · via DEV Community

You have been writing Elixir for a while. You know GenServers. You know supervisors. You have hit the BEAM's concurrency model enough times to trust it. But there is a level below that which most Elixir developers never look at, and it explains a lot of behavior that otherwise seems like magic.

Why does spawning a hundred thousand processes not kill your system? Why is a process crash isolated but not silent? Why does garbage collection in Elixir not stop the world? The answers are all in the BEAM, and they are worth understanding properly.

The BEAM Is Not the JVM With a Different Language

This comparison comes up constantly and it misleads people. Both are virtual machines that run bytecode. That is roughly where the similarity ends.

The JVM was designed around threads. Concurrency on the JVM means OS threads, shared mutable state, locks, and a garbage collector that operates across the entire heap. The BEAM was designed around processes. Not OS processes. Not OS threads. Its own lightweight processes, each with isolated memory, independent garbage collection, and message passing as the only communication mechanism.

This is not an implementation detail. It is the fundamental design decision that every other property of the BEAM follows from.

Processes Are Cheaper Than You Think

When people hear "process" they think OS process: expensive, slow to start, heavy on memory. BEAM processes are none of those things.

A BEAM process starts with around 2KB of stack space. It grows as needed. Spawning one takes microseconds. The BEAM scheduler manages thousands of them across a small pool of OS threads, one per CPU core by default. You can run a million concurrent processes on a single machine without the OS knowing about most of them.

# This is not dangerous. It is idiomatic.
pids = Enum.map(1..100_000, fn i ->
  spawn(fn ->
    Process.sleep(:timer.seconds(10))
    IO.puts("Process #{i} done")
  end)
end)

IO.puts("Spawned #{length(pids)} processes")
# => Spawned 100000 processes

Enter fullscreen mode Exit fullscreen mode

On most systems this runs without issue. Try the equivalent with OS threads in any language and you will run out of resources before you hit ten thousand. The difference is not hardware. It is the scheduler.

The Scheduler: Preemptive and Reduction-Based

The BEAM uses a preemptive scheduler, but not in the way most runtimes do it. It does not preempt based on time slices measured in milliseconds. It preempts based on reductions.

A reduction is roughly one unit of work: a function call, a pattern match, a message send. Each process gets a budget of 2000 reductions per scheduling turn. When the budget runs out, the scheduler suspends the process and runs another one. This happens regardless of what the process is doing. A tight loop does not starve other processes.

# This does not block the scheduler
defmodule Spinner do
  def spin(n) do
    spin(n + 1)  # each call consumes a reduction
  end
end

spawn(fn -> Spinner.spin(0) end)
spawn(fn -> IO.puts("I still run") end)
# => I still run

Enter fullscreen mode Exit fullscreen mode

In Node.js, a synchronous tight loop blocks the event loop entirely. In the BEAM, the scheduler preempts it after 2000 reductions. Every other process continues running. The misbehaving process gets its turn again shortly, but it cannot monopolize the system.

This is why latency in Elixir systems tends to be consistent rather than spiky. No single process can hold the scheduler hostage.

Memory Isolation and Per-Process GC

Every BEAM process has its own heap. No sharing. When a process dies, its heap is reclaimed immediately. No coordination with other processes, no waiting for a global GC cycle.

This is the source of one of the BEAM's most important properties: garbage collection does not stop the world. There is no world to stop. Each process collects its own garbage independently, on its own schedule, without affecting any other process.

# Each process allocates and collects independently
spawn(fn ->
  large_list = Enum.to_list(1..1_000_000)
  # large_list is only in this process's heap
  # GC here affects nothing else
  :ok
end)

# This process is unaffected by the GC above
spawn(fn ->
  IO.puts("Running without pause")
end)

Enter fullscreen mode Exit fullscreen mode

Compare this to the JVM's stop-the-world pauses, or Go's GC which, while incremental, still coordinates across the entire heap. In the BEAM, a process doing heavy allocation and collection does not introduce latency spikes in other processes. The isolation is complete.

The tradeoff is message passing. Because processes share no memory, sending data between them requires copying. Large messages are expensive not because of the send itself but because of the copy. This is why Elixir idioms tend toward small, frequent messages rather than large data transfers. The design pushes you toward the pattern that performs well.

How Message Passing Actually Works

Every process has a mailbox. Sending a message puts a copy of the message into the recipient's mailbox. The recipient reads from its mailbox using receive. This is asynchronous by default: sending does not block.

parent = self()

spawn(fn ->
  send(parent, {:result, 42})
end)

receive do
  {:result, value} -> IO.puts("Got: #{value}")
after
  5000 -> IO.puts("Timed out")
end

Enter fullscreen mode Exit fullscreen mode

The receive block pattern matches against messages in the mailbox in order. If no message matches, the process blocks and waits. Unmatched messages stay in the mailbox. This is a subtle point: if you receive messages without handling all patterns, your mailbox grows unboundedly. A common bug in long-running processes is accumulating unmatched messages until memory pressure becomes a problem.

# Dangerous in a long-running process
receive do
  {:ok, value} -> handle(value)
  # no catch-all: anything else stays in mailbox forever
end

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

Enter fullscreen mode Exit fullscreen mode

Preemption vs Cooperation: The NIF Problem

The BEAM's scheduler is preemptive for Elixir and Erlang code. It is not preemptive for NIFs.

A NIF (Native Implemented Function) is C code called directly from Elixir. When a NIF runs, the BEAM scheduler cannot preempt it. The OS thread running that NIF is blocked for the duration of the call. If your NIF takes 100ms, every process scheduled on that OS thread waits 100ms.

# If :my_nif.heavy_computation/1 takes 500ms,
# it blocks the scheduler thread for 500ms
result = :my_nif.heavy_computation(data)

Enter fullscreen mode Exit fullscreen mode

This is why the BEAM documentation is so emphatic that NIFs must be fast. Anything over one millisecond should use a dirty scheduler: a separate pool of OS threads reserved for long-running native calls that runs outside the main scheduler.

# In your NIF definition (C side), mark it dirty:
# ERL_NIF_DIRTY_JOB_CPU_BOUND or ERL_NIF_DIRTY_JOB_IO_BOUND

Enter fullscreen mode Exit fullscreen mode

Understanding this explains why some Elixir libraries that wrap C code (image processing, certain crypto operations, database drivers with native extensions) can introduce latency spikes that seem inconsistent with the BEAM's concurrency model. They are bypassing the model.

The Scheduler's Relationship With IO

IO in the BEAM is non-blocking at the scheduler level. When a process does a network read or a file operation, the BEAM does not block the OS thread. It suspends the process, registers the IO operation with an internal polling mechanism (built on epoll or kqueue depending on the OS), and runs other processes. When the IO completes, the process is rescheduled.

This is why Elixir handles tens of thousands of concurrent connections without a thread-per-connection model. Each connection maps to a process. Each process consumes a BEAM process worth of resources (a few kilobytes), not an OS thread. The IO multiplexing is handled by the runtime, not by the application code.

# Each of these connections is a lightweight process
# The BEAM handles the IO multiplexing
{:ok, socket} = :gen_tcp.accept(listen_socket)
pid = spawn(fn -> handle_connection(socket) end)

Enter fullscreen mode Exit fullscreen mode

Phoenix handling millions of concurrent WebSocket connections is not a framework trick. It is the BEAM's process model applied to network IO with no special configuration required.

What This Means for How You Write Code

Once you understand the BEAM's process model, certain Elixir patterns stop looking like conventions and start looking like direct expressions of the runtime's capabilities.

Spawning a process per request is not wasteful, it is appropriate. Letting a process crash instead of handling every possible error is not lazy, it is correct: the process's isolated heap disappears cleanly, the supervisor restarts it in a known good state, and no other process was affected.

The things that are expensive in other runtimes (concurrency, isolation, failure recovery) are cheap in the BEAM because the runtime was built to make them cheap. The things that are expensive in the BEAM (large message copies, long-running NIFs) are expensive because they work against the model.

The more directly your code expresses the model, the more you get out of it.

Further Reading