The first three articles in this series built up the BEAM’s runtime model piece by piece - what concurrency is, where functional JavaScript is slow, how currying exposes a language’s function model. This article asks what you do with that model in a real system. It has two halves: why Elixir’s runtime is exceptionally good at orchestration - coordinating moving parts, keeping them alive, and recovering from failures - and when you should not do the work in Elixir. The BEAM is deliberately not the fastest tool for raw computation, so the idiomatic choice is to delegate that work to a language built for it.
Both halves follow from the same fact: the BEAM optimizes for coordination and resilience, not for instruction-level throughput. That is not a weakness. It is why Elixir fits the orchestration job, and why a mature Elixir system is polyglot by design: Elixir is the conductor, and other languages are the instruments.
What orchestration means
Orchestration is the part of a system that isn’t the computation. It is the coordination layer: accepting a request, routing it, fanning work out to the parts that need to run in parallel, gathering results, holding shared state, retrying failed work, timing out hung work, and keeping the system running through partial failures and deploys. A web server holds a hundred thousand WebSocket connections open and broadcasts to them. A background-job system enqueues work in a transaction, runs it with a concurrency limit, and cancels it mid-execution when a newer job supersedes it. A real-time presence tracker knows who’s online across a cluster of machines.
None of these jobs is CPU-heavy in the algorithmic sense. They are I/O-heavy and coordination-heavy: lots of waiting, juggling, and decisions about what happens when one part dies. That is the workload the BEAM was built for. Ericsson created it in the 1980s for telephone switches that had to run for years, handle millions of concurrent calls, and survive the failure of any single component. As Elixir in Action puts it, the goal of the platform “isn’t to squeeze out as many requests per second as possible but to keep performance as predictable and within limits as possible.” Predictability under load matters in orchestration. The BEAM chose it.
Why Elixir excels at it
Three runtime properties, all covered in depth in the concurrency article, make Elixir a natural orchestration layer. Here is what each one means for orchestration.
A process per job, cheap enough to hand out freely
The BEAM’s processes are lightweight - a couple of microseconds to spawn and roughly 2 KB to start - so give one to every connection, request, background job, and session. The theoretical limit is around 134 million; a few hundred thousand is unremarkable. This is the foundation. Orchestration manages many concurrent things, and a runtime where “a new concurrent thing” costs 2 KB makes one process per thing the obvious, cheap design. Do not pool processes the way you pool database connections. Make a new one.
Processes share no memory and communicate by copying messages. That removes the whole class of shared-state bugs - locks, races, and deadlocks that one Erlang book calls “the GOTO of our time.” Coordination happens through message passing, asynchronous by default, and it works the same way whether the receiver is on the same machine or a clustered node across the network. As Erlang and OTP in Action notes, “in real life, you can’t share data over the wire - you can only copy it. Erlang’s process communication always works as if the receiver gets a personal copy of the message, even if the sender happens to be on the same computer.” Distribution is not a separate concern. It is the same message-passing you use locally.
Stateful services that don’t fight you
A coordinated system needs shared state: caches, registries, “who’s online,”
session data, and counters. The Node default is the module-level singleton
(const cache = new Map()), which is safe within a synchronous section because
the single-threaded event loop guarantees no interleaving. It is invisible to
other processes, though, and becomes inconsistent when you scale to a cluster
or run multiple instances behind a load balancer. The BEAM takes the opposite
approach: a GenServer is a process that owns its state and handles one
message at a time. Access is serialized by structure, not by accident. Run
many independent GenServers for concurrency. When one is overloaded, messages
accumulate in its mailbox - a visible, measurable backpressure signal instead
of a silent stall.
For state shared across many processes, the BEAM provides ETS (Erlang Term
Storage): VM-managed in-memory key/value tables shared between processes on a
node, with tunable fine-grained concurrency - write_concurrency: :auto on
recent OTP automatically adjusts synchronization granularity at runtime. (ETS
is powered by C code under the hood. It is an early example of the BEAM
delegating hot paths to native code - more on that pattern below.) For
demand-driven flow control, GenStage and Broadway implement pipelines
where consumers send demand upstream and producers emit only when asked -
backpressure inside the application VM, with no external broker required.
The ecosystem consequence is clear in background jobs. Node’s conventional answer, BullMQ, requires Redis - a new stateful infrastructure dependency - while each idle Node worker holds its own blocking Redis connection. Elixir’s Oban stores jobs in the PostgreSQL (or MySQL or SQLite) database the application already has. Jobs gain transactional enqueueing (they commit or roll back with the application data), per-queue concurrency limits, and the ability to cancel a job in the middle of execution regardless of which node runs it. A job is a process, and the VM can kill a process. Cancellation on the BEAM is enforced by the runtime, not requested as a convention that each API might honor.
Supervision: recovery as architecture
This is what makes Elixir genuinely better at orchestration than systems built on “stateless services behind a load balancer.” It deserves the most space.
In Node, an unhandled exception takes down the entire OS process. Every in-flight request and connection on it is lost. The standard answer - stateless services, external state in Redis or a database, replicas, PM2/systemd/Kubernetes restarts - is proven and runs most of the industry. A process restart cannot restore lost in-flight work. It also cannot isolate partial state corruption within a process. When the process dies, everything it was doing dies, guilty and innocent alike.
The BEAM’s model is let it crash, and the name misleads people. It does not mean “don’t write error handling.” As the BEAM’s own teaching material puts it: “at the level of an individual process, prefer crashing to defensive recovery; centralize recovery at the supervisor level.” A process that hits an unexpected state does not know what the rest of the system expects. In-place recovery can make things worse: half-written state, half-sent messages, and a system in a shape nobody designed for. A supervisor knows the system’s structure. It can restart just this one process, restart its siblings, restart the whole subsystem, or give up and escalate.
A stateful GenServer sits under a supervisor that restarts it with clean state
when it fails. That combination lets “stateful singleton” and “crash-only
recovery” work together instead of conflict - something structurally awkward
in a stateless-services world, where holding state in-process and recovering
from crashes are at odds. Supervisors have explicit strategies:
:one_for_one restarts only the dead child; :one_for_all restarts all
children (for when they share fate); :rest_for_one restarts the dead child
and everything started after it. A restart intensity decides how many
restarts in how many seconds occur before the supervisor gives up and
escalates to its supervisor. The system is a tree. Failures bubble up to the
supervisor that knows what to do, and the application’s top-level supervisor
is the final authority.
This is orchestration in the deepest sense: not “run these tasks,” but keep the system running through inevitable failures and recover them at the right granularity. Phoenix’s two-million-connection benchmark, WhatsApp’s roughly two-million-connections-per-server, and Discord’s reported eleven-million concurrent users are orchestration feats. They rely on a runtime where one failed connection is one process that a supervisor can restart while the other 1,999,999 keep running. Fault tolerance on the BEAM is not something you bolt on with retries and circuit breakers. The supervision tree gives you its shape from the start.
When to reach for another language
Here is the other half of the same trade-off. Everything above makes Elixir exceptional at coordination, and coordination is not computation. The reduction-budget scheduler that gives the BEAM its fairness - every process is preempted after ~2,000 reductions so none can starve the others - is overhead a hot loop pays on every function call. V8’s optimizing JIT, by contrast, is built to make one thread execute one tight loop as fast as physically possible. As Elixir in Action states it in a section titled “Speed”: “intensive CPU computations aren’t as performant as, for example, their C/C++ counterparts, so you may consider implementing such tasks in some other language and then integrating the corresponding component into your Erlang system. If most of your system’s logic is heavily CPU bound, you should probably consider some other technology.”
That is the same trade-off from the other direction. The practical answer is use Elixir for the shape of the system, and use a language built for compute for the hot inner loops. The BEAM has always done this itself (ETS is C under the hood), and the ecosystem gives you three well-trodden ways to do it.
Ports: the safe, isolated escape hatch
The simplest and safest way to call foreign code is a port: a separate OS program connected to the BEAM by its standard input and output, exchanging data as a byte stream. The foreign program has its own address space, so “no matter what the program does, it can’t crash the running Erlang system.” If it crashes, the BEAM detects it and can restart it - supervised, like any other process. Erlang and OTP in Action‘s guidance is direct: “Whenever in doubt, you should always start with a plain implementation using ports and then optimize later if it turns out that you need more speed.”
The trade is straightforward: “to get this level of safety, you pay a price in speed. All data that moves between the two processes must be passed as a byte stream. You may need to define your own byte-oriented protocol, with marshalling and unmarshalling of data.” For work that costs far more than marshalling - image processing, a heavy ML inference call, a big parse - that overhead is noise. For cheap, frequent work, it matters. Use the next option.
NIFs: speed in exchange for trust
A NIF (Native Implemented Function) makes the opposite trade. The native function runs inside the VM, in the context of the scheduler thread that calls it, with direct access to BEAM data structures and minimal call overhead. It is called like any ordinary function. The catch is the one Erlang and OTP in Action spells out: “NIFs have minimal communication overhead; but a single bug in your NIF code can easily crash the entire Erlang VM, so they shouldn’t be used willy-nilly - only when you’re certain they’re the right solution.” Historically, “the native function runs in the context of the VM thread that calls it, and the thread can’t be rescheduled until the NIF has returned… this makes NIFs suitable only for functions that execute and return quickly; long-running NIFs hold up resources for the Erlang VM scheduler.”
That last problem is why dirty schedulers exist. Long-running native work can run on a separate dirty-scheduler thread instead of stalling the normal ones. (As Elixir in Action notes, “in some cases, long-running CPU-bound work or a larger garbage collection might be performed on another thread (called a dirty scheduler).”) With dirty NIFs, the safety concern shrinks to crashing the VM rather than stalling it. That is still serious. The throughput case is clear: write the compute kernel in a language with no GC, call it from Elixir, and let the BEAM orchestrate everything around it.
Which language? The classic NIF literature assumes C and warns about the problem C creates: code “written in C, a language that leaves a lot of the error checking and resource management to the developer… is much less reliable; and because the code is linked directly into the Erlang VM, any error has the potential to bring down the entire Erlang system.” That warning is the argument for Rust. A memory-safe systems language gives you NIF speed with a far smaller chance of the memory-corruption bug that turns “fast” into “the whole VM is down.” This is the modern shape of the polyglot BEAM: Rustler wraps Rust NIFs with safe bindings; Zigler does the same for Zig. The famous reported example is Discord, whose Elixir gateway is said to use Rust NIFs for the CPU-heavy member-list data structures behind its millions-of-concurrent-users real-time presence - taking the orchestration (holding the connections, routing messages, supervising the works) in Elixir and the data-structure crunching in Rust. (The Discord detail is community-reported; treat the specific “Rust NIFs for member-list” claim as widely-circulated rather than independently verified here, but the architectural pattern it illustrates - Elixir conducts, Rust computes - is exactly the one the runtime is designed for.)
The synthesis: best of both worlds
The framing Erlang and OTP in Action uses for the JSON-parser example, after walking through the port-vs-NIF choice, applies to the whole decision: “This will let you have the best of both worlds: you get to use a fast library to do the parsing, while you keep the bulk of your code in Erlang.” Replace “parsing” with “image processing,” “ML inference,” “cryptographic hashing,” “video transcoding,” or another genuinely CPU-bound kernel, and the point is the same. Elixir handles the orchestration: accepting the request, authorizing it, holding the user’s session, fanning out the work, retrying on failure, broadcasting the result over a WebSocket, and surviving a deploy. The compute moves to the language best at that compute, across a boundary whose cost the BEAM has spent thirty years making predictable.
When Elixir is the wrong tool
An honest article names the cases where Elixir is not the answer. They follow from the same trade-off.
If your system is mostly CPU-bound - a batch image processor, a model training pipeline, a compile farm, a numerical simulator - the orchestration layer is a small part of the work, while the BEAM’s throughput tax applies to most of it. Elixir in Action‘s exit criterion stands: “if most of your system’s logic is heavily CPU bound, you should probably consider some other technology.” Reach for Rust, Go, C++, or a numerics stack directly. Do not pay reduction-budget overhead for a workload that never benefits from fairness.
If your system is start-and-finish per request with no long-lived connections and no coordination - a stateless JSON API, a serverless function, an SSR render - Node’s single fast thread with its optimizing JIT is hard to beat, and the cold-start numbers favor it decisively (Node Lambda cold starts around 315 ms versus community-reported ~1.2 s for Elixir, with the Elixir community’s own consensus being that serverless “doesn’t leverage any of the strong points of BEAM”). The BEAM’s strengths are wasted on a workload that spins up, answers, and exits.
If your team’s expertise and ecosystem decide the choice - npm’s roughly two million packages, the full-stack TypeScript story, and the hiring pool - those are legitimate reasons. They have nothing to do with runtime properties and everything to do with shipping software. Sometimes the best runtime is the one your team can staff and operate.
The honest summary
Orchestration is the part of a system that isn’t the computation: coordination, state, recovery, and staying up. Elixir excels at it for three compounding reasons. Processes are cheap enough to hand out freely (one per connection, per request, per job). Stateful services fit the runtime instead of fighting it. Supervision makes recovery an architectural decision instead of an operational afterthought. The same runtime choices that create those strengths - the reduction budget, the per-process heaps, and message-copying - mean the BEAM is deliberately not the fastest at raw instruction throughput. A mature Elixir system accepts that by being polyglot: Elixir orchestrates, the right compute language handles the hot loops, and the call crosses a port or a NIF whose trade-offs (safety versus speed, isolation versus overhead) the runtime makes predictable.
The technique is the same on both sides: know what each part of your system needs to be good at, and let it be that. Use Elixir for what Elixir is for - keeping many concurrent things alive and coordinated through partial failures - and reach for Rust or C or a numerics stack when a piece of work is genuinely compute-bound. The BEAM is not trying to be the fastest language for everything. It is trying to be the best conductor for a system whose fast parts are written in something else. At that job, it has few peers.
Where to go next
- Process Ring - orchestration’s atomic unit as an exercise: one process per job, wired into a ring, passing a token.
- Print in Order (concurrent) - the coordination half of orchestration: three processes that must fire in sequence, ordered by messages.
- Publish-subscribe - the pattern the orchestrator leans on: producers and consumers decoupled by a bus instead of wired to each other.