Elixir
Elixir is a functional, dynamically typed language that compiles to bytecode for the BEAM, the virtual machine built for Erlang at Ericsson to run telephone switches that could not go down. It inherits from Erlang a concurrency model that is genuinely different from everything else in mainstream use, and wraps it in modern syntax, tooling, and a build system people enjoy using.
The core idea: an Elixir program is thousands or millions of tiny isolated processes (not OS processes — the BEAM's own, ~2 KB each) that share nothing and communicate only by sending messages. A process that crashes takes nothing with it; a supervisor notices and restarts it in a known-good state. This is why the ecosystem's slogan is "let it crash" — not carelessness, but a system design where failure is a routine, contained event rather than an exception to be defended against everywhere.
TL;DR
- Elixir runs on the BEAM: lightweight preemptively scheduled processes, share-nothing isolation, message passing.
- Immutable data + pure functions — no shared mutable state, so no locks, no data races, and no mutex-related bugs.
- OTP supplies the battle-tested building blocks:
GenServer,Supervisor,Task,Registry,ETS. - "Let it crash": don't defensively code every failure — isolate it in a process and let a supervisor restart from a clean state.
- Preemptive scheduling means one slow process cannot starve the rest — soft real-time, consistently low tail latency.
- Phoenix + LiveView deliver real-time interactive UIs with server-rendered state and little JavaScript.
- Excellent for concurrent, IO-bound, connection-heavy systems; weak for CPU-bound numeric work — call out to Rust via NIFs.
Quick Example
Pattern matching, pipelines, and a supervised stateful process:
Pattern matching and the pipe operator, which define the day-to-day feel of the language:
Core Concepts
Processes, not threads
A BEAM process is not an OS thread. It starts at roughly 2 KB, has its own heap and garbage collector, and is scheduled preemptively by the VM across one scheduler thread per CPU core.
Four consequences that shape everything else:
- Isolation. Processes share no memory. A crash cannot corrupt another process's state.
- Per-process GC. Collection pauses one small heap, not the world — so there are no global stop-the-world pauses and tail latency stays flat.
- Preemption. The scheduler interrupts a process after a fixed work budget. One process in a tight loop cannot block the others — unlike cooperatively scheduled runtimes (Node's event loop, Python's asyncio) where a single blocking call stalls everything.
- Distribution.
send(pid, msg)works identically whether the process is local or on another node in the cluster.
Message passing
Every process has a mailbox. receive pattern-matches messages, taking the first that matches and leaving the rest. Messages are copied, never shared — which is precisely why there are no data races and why the model scales across machines unchanged.
Immutability and pattern matching
Pattern matching in function heads replaces most conditionals. It also encodes intent: writing {:ok, value} = do_work() asserts that failure is impossible here, and if that assumption breaks you find out at the crash site instead of three layers away.
OTP: the standard library for reliability
OTP is a set of behaviours (interfaces with a generic implementation) encoding decades of production experience. You implement callbacks; OTP handles the concurrency, timeouts, and lifecycle correctly.
Supervision trees and "let it crash"
"Let it crash" applies to unexpected errors — a malformed payload, an unreachable dependency, a bug. Rather than wrapping everything in defensive try/rescue that guesses at recovery, you let the process die and let the supervisor restart it from a state you know is valid. Expected outcomes ("user not found") are still modeled explicitly as {:error, reason} tuples. The distinction is intent, not laziness.
💡 Tip: Supervisors have restart intensity limits (default: 3 restarts in 5 seconds). Exceeding it escalates the failure to the parent supervisor, and eventually shuts the application down. A crash loop surfaces loudly instead of spinning silently.
The Ecosystem
Phoenix and LiveView
LiveView is the ecosystem's signature feature. The server holds UI state in a process, renders HTML, and pushes minimal diffs over a websocket; events from the browser come back as messages. You get an interactive SPA-grade experience with no client-side state management and no API layer to maintain.
Each connected user gets their own supervised process holding their UI state. The BEAM's cheap processes are what make that economically sensible — and it's why LiveView exists here and not in most other ecosystems. The tradeoff: it requires a persistent connection and server-held state per user, so it fits interactive applications far better than content sites or offline-capable apps. See WebSockets and Local-First.
Comparison
Choose Elixir when you're holding many concurrent connections (chat, presence, IoT, multiplayer, live dashboards), you need consistently low p99 latency under load, or fault isolation and uptime are explicit requirements. Choose something else when the work is CPU-bound numeric computation, the team has no functional-programming appetite, you need a rich SDK for a niche vendor, or hiring locally would be hard.
Elixir vs. Erlang: identical runtime and full interop — you can call Erlang modules directly (:crypto.hash/2). Elixir adds friendlier syntax, macros, protocols, mix, and a much larger modern library ecosystem. New projects on the BEAM overwhelmingly choose Elixir.
Best Practices
Model failure in the type of the return, not in exceptions
Reach for Task before GenServer
Not every concurrent operation needs a stateful server. If you just need parallel work, Task.async_stream/3 is the right tool and handles backpressure and timeouts for you.
Never let a single GenServer become a bottleneck
A GenServer processes messages one at a time. Routing all traffic through one is a global serialization point — the most common performance mistake in Elixir. Partition by key with Registry, use a pool, or move read-heavy state into :ets, which supports concurrent lock-free reads.
Keep long-running work out of init/1
init/1 blocks the supervisor's start sequence. Return quickly and defer the expensive part:
Always set timeouts on GenServer.call/3
The default is 5 seconds, and a call that exceeds it raises in the caller. For genuinely slow operations, raise the timeout deliberately or restructure as an async cast plus a reply message — don't let a slow dependency cascade into caller crashes.
Use Ecto changesets for all external input
Instrument with Telemetry from the start
Phoenix, Ecto, and Oban all emit :telemetry events. Attach handlers and export to Prometheus or OpenTelemetry — and keep Phoenix.LiveDashboard mounted behind auth for live process, memory, and ETS inspection.
Common Mistakes
A single GenServer serializing everything
Defensive try/rescue instead of supervision
Unbounded mailbox growth
Atoms from user input
Building lists with ++ in a loop
Blocking the caller with slow work in handle_call
FAQ
Is Elixir fast?
For IO-bound and concurrent workloads, yes — and more importantly, it's consistently fast: p99 latency stays flat under load because scheduling is preemptive and GC is per-process. For raw single-threaded CPU throughput it's slower than Go, Java, or Rust. The standard remedy is to keep the orchestration in Elixir and push hot numeric kernels into Rust via Rustler NIFs or into Nx.
Do I need to learn Erlang first?
No. Elixir is complete on its own, and you'll pick up the Erlang you need incidentally — calling :crypto, :ets, or :timer is routine and the syntax is obvious in context. Reading Erlang becomes useful when you go deep on OTP internals or read library source.
Is Elixir statically typed?
It's dynamically typed today, with Dialyzer providing success-typing analysis over optional @spec annotations. The language is incrementally gaining a native set-theoretic type system, rolled out gradually across releases — pattern-matching-aware inference is already catching real bugs at compile time.
What is LiveView actually good for?
Interactive, authenticated, connection-friendly applications: dashboards, admin tools, internal apps, forms with live validation, collaborative and presence-driven features. It's a poor fit for public content sites needing perfect SEO with no connection, offline-capable apps, and anything that must work with the tab closed.
How does Elixir handle deployment?
mix release builds a self-contained release bundling the BEAM, your compiled code, and config — no runtime installation needed on the target. It runs well in Docker and on Kubernetes; clustering across nodes uses libcluster. Hot code upgrades exist and are genuinely used in some deployments, but most teams do ordinary rolling deploys.
Who uses Elixir in production?
Discord (millions of concurrent users, with hot paths in Rust NIFs), Pinterest, Heroku, PepsiCo, Brex, and a long tail of companies doing messaging, IoT, fintech, and real-time collaboration. It's a deliberate, niche-but-durable choice rather than a default one.
Related Topics
- Programming Languages — where Elixir sits in the landscape
- Go — the other concurrency-first choice, with a different model
- Rust — the usual partner for CPU-bound NIFs
- Concurrency Patterns — the general problems OTP solves
- WebSockets · Server-Sent Events — the connection-heavy workloads Elixir excels at
- Microservices — why OTP applications often replace several of them
- PostgreSQL — Ecto's primary target, and Oban's job store
- Background Jobs — Oban's domain
- High Availability — supervision trees as an availability strategy
- Functional patterns in Design Patterns — how OTP reframes classic patterns
- Phoenix Framework — The web framework built on the BEAM