Elixir
First appeared 2012 · José Valim
Modern syntax on a runtime built for systems that are not allowed to go down.
Overview
Elixir is a dynamic, functional language that runs on the Erlang virtual machine, inheriting three decades of engineering aimed at systems that must not go down. Processes are extremely lightweight — hundreds of thousands per machine is routine — and are isolated, so one crashing cannot corrupt another. Supervision trees restart failed processes automatically, which is the practical expression of Erlang's 'let it crash' philosophy: rather than defending against every possible error, you structure the system so failures are contained and recovered from. Elixir adds a modern, readable syntax, a powerful macro system, first-class tooling in Mix and Hex, and excellent documentation conventions. Phoenix, its web framework, is known for very low latency and for LiveView, which builds interactive interfaces with server-rendered state and no client-side framework.
Key facts
The reference details, without the paragraph.
- First appeared
- 2012
- Designed by
- José Valim
- Typing
- Dynamic and strong, with a gradual set-theoretic type system arriving incrementally
- Execution
- Compiled to BEAM bytecode, run on the Erlang virtual machine
- Memory model
- Per-process heaps with independent garbage collection — no global pause
- Package manager
- Mix with Hex
- File extensions
- .ex, .exs
- Concurrency
- Lightweight isolated processes with message passing, not threads
- Fault tolerance
- Supervision trees restart failed processes automatically
- Licence
- Apache 2.0
History
How the language got here — the decisions that still shape how you write it.
Elixir was created by José Valim and released in 2012. Valim was a Rails core team member who had spent years working on Ruby's concurrency story and concluded that the runtime itself was the limiting factor. Rather than fight it, he looked for a virtual machine that had already solved the problem and found Erlang's BEAM, built at Ericsson in the 1980s for telephone switches expected to run for years without interruption. Erlang's concurrency and fault tolerance were exactly what modern web systems needed; its syntax and tooling were the barrier to adoption. Elixir was designed to remove that barrier — a familiar, expressive syntax with modern tooling on top of a runtime with an extraordinary production record. The Phoenix framework followed in 2014 and demonstrated the point concretely, handling two million simultaneous WebSocket connections on a single machine in a widely-discussed 2015 benchmark. LiveView, introduced in 2019, went further by questioning whether most applications need a client-side framework at all.
- 1986
Erlang is built at Ericsson
Joe Armstrong and colleagues create Erlang for telephone switches expected to run for years without interruption. The concurrency and fault-tolerance model Elixir inherits is designed here.
- 2011
A Rails core member goes looking for a runtime
José Valim, working on Ruby's concurrency problems, concludes the runtime is the constraint and starts prototyping a language on the BEAM instead.
- 2012
Elixir 1.0 direction set
The design goal is explicit: Erlang's runtime guarantees with a syntax, tooling and documentation culture that people will actually adopt.
- 2014
Phoenix arrives
The web framework demonstrates the runtime's advantages concretely, with response times measured in microseconds and channels for real-time features built in.
- 2015
Two million connections on one machine
A widely-discussed Phoenix benchmark sustains two million simultaneous WebSocket connections on a single server, making the BEAM's concurrency model tangible to people outside the Erlang world.
- 2019
LiveView
Interactive interfaces with state on the server and diffs over a WebSocket, questioning whether most applications need a client-side framework at all.
- 2022–2025
Types, Nx and machine learning
Valim begins landing a gradual set-theoretic type system, while the Nx project brings numerical computing and machine learning to the BEAM.
What it is good at
The reasons teams pick it, stated concretely.
Concurrency that is genuinely cheap
A process costs a few hundred bytes and starts in microseconds, so hundreds of thousands per machine is ordinary. Each has its own heap and garbage collector, so one process collecting does not pause the others.
Failure is designed for, not defended against
Supervision trees restart crashed processes into a known-good state. Instead of defensive code guarding every possible error, you isolate failures and recover — which produces markedly simpler code in the happy path.
Predictable latency under load
The BEAM preemptively schedules processes, so one long-running computation cannot starve the rest. This is why Elixir services tend to have flat tail latency where other runtimes spike.
Phoenix and LiveView
LiveView removes the client-side framework, the API layer and the state synchronisation between them for a large class of applications. For a small team that is an enormous reduction in moving parts.
Excellent tooling and documentation culture
Mix, Hex, ExUnit, doctests and formatter all ship together. Documentation is written in the source as a first-class construct, and the community norm of documenting well is unusually strong.
Trade-offs
Every language costs you something. Knowing what, before you commit, is the whole point.
Slow at raw number crunching
The BEAM is optimised for concurrency and message passing, not floating-point throughput. CPU-bound numerical work is far slower than in Rust, C or even Java — Nx and native implemented functions exist for exactly this reason.
A genuinely different mental model
No mutable state, no shared memory, no objects, and processes rather than threads. Experienced developers from imperative languages often find the first weeks disorienting in a way that learning, say, Go does not produce.
A small hiring pool
Teams that adopt Elixir generally have to train people into it. That works well in practice — the language is not hard once the model clicks — but it is a real consideration for a growing organisation.
A smaller library ecosystem
Hex covers the common needs well, but for a specialised SDK or an obscure protocol you are more likely to be writing the client yourself than in Python or JavaScript.
Dynamic typing, for now
Large Elixir codebases rely on Dialyzer, which is slow and reports errors in a notoriously opaque way. The set-theoretic type system is arriving gradually and will improve this substantially, but it is not complete.
Code examples
Not syntax tours — the idioms that make code read like the language rather than a translation of another one.
# `=` is a match operator, not assignment.
{:ok, config} = load_config("app.exs")
# Function clauses match on shape — no conditionals inside the body.
def handle({:ok, %User{admin: true} = user}), do: {:admin, user.name}
def handle({:ok, %User{} = user}), do: {:member, user.name}
def handle({:error, :not_found}), do: {:error, "no such user"}
def handle({:error, reason}), do: {:error, inspect(reason)}
# The pipe operator threads a value through transformations.
" Hello World "
|> String.trim()
|> String.downcase()
|> String.replace(~r/[^a-z0-9]+/, "-")
|> String.trim("-")
# => "hello-world"defmodule Counter do
use GenServer
# Client API — runs in the caller's process.
def start_link(initial), do: GenServer.start_link(__MODULE__, initial, name: __MODULE__)
def increment, do: GenServer.cast(__MODULE__, :increment) # fire and forget
def value, do: GenServer.call(__MODULE__, :value) # waits for a reply
# Server callbacks — run in the GenServer's own process.
@impl true
def init(initial), do: {:ok, initial}
@impl true
def handle_cast(:increment, count), do: {:noreply, count + 1}
@impl true
def handle_call(:value, _from, count), do: {:reply, count, count}
enddefmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.Repo,
{Phoenix.PubSub, name: MyApp.PubSub},
{Counter, 0},
MyAppWeb.Endpoint
]
# :one_for_one restarts only the child that died.
# :rest_for_one would also restart everything started after it.
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
end# Run independent calls concurrently and collect the results.
results =
["/profile", "/orders", "/settings"]
|> Task.async_stream(&HTTP.get/1, max_concurrency: 10, timeout: 5_000)
|> Enum.map(fn
{:ok, {:ok, body}} -> body
{:ok, {:error, _}} -> nil
{:exit, :timeout} -> nil # a task that timed out is reported, not raised
end)
|> Enum.reject(&is_nil/1)
# `with` chains operations that each return {:ok, _} or {:error, _}.
with {:ok, user} <- fetch_user(id),
{:ok, account} <- fetch_account(user),
:ok <- verify_active(account) do
{:ok, account}
else
{:error, reason} -> {:error, reason}
:suspended -> {:error, :account_suspended}
endCommon pitfalls
The mistakes that cost everyone an afternoon at least once.
Treating a GenServer as a place to put everything
A single GenServer serialises every request through one process, making it a bottleneck. Use one per unit of state that genuinely needs isolation, not one per module.
Blocking inside handle_call
The process handles one message at a time, so a slow HTTP request inside a callback blocks every other caller. Do the work in a Task, or reply first and continue in handle_info.
Rescuing exceptions reflexively
Wrapping everything in try/rescue defeats supervision. Let processes crash and be restarted; reserve rescue for genuinely recoverable, expected conditions.
Atoms from user input
Atoms are never garbage collected. `String.to_atom(user_input)` will eventually exhaust the atom table and bring the node down. Use `String.to_existing_atom/1`.
Building strings with ++ in a loop
List concatenation copies the left operand each time, so appending in a loop is quadratic. Prepend and reverse, or build an iolist and let the runtime flatten it once.
Forgetting the default call timeout
`GenServer.call` gives up after five seconds and exits the caller. If the operation legitimately takes longer, pass an explicit timeout or restructure it as an async task.
In production
Where it is running at scale, and what it is doing there.
Discord
Real-time messaging for millions of concurrent users, built on Elixir and the BEAM.
Pinterest
Notification and spam-detection systems, replacing a larger Java deployment.
Heroku
Routing infrastructure handling very high connection volumes.
Bleacher Report
Replaced a large Rails deployment with a fraction of the servers.
Learning path
A realistic order to learn things in, with something to build at each step.
- 1
Week 1
The functional basics
Immutability, pattern matching, the pipe operator, function clauses and guards, and the core data types — tuples, lists, maps and structs. Resist writing imperative loops; use Enum and recursion.
Build this: Write a script that parses a log file and reports the most frequent errors, using only pipelines.
- 2
Week 2
Modules, protocols and tooling
Modules and behaviours, protocols for polymorphism, Mix projects, ExUnit and doctests. Elixir's documentation conventions are worth adopting from the first project.
Build this: Package your script as a Mix project with tests and documented public functions.
- 3
Weeks 3–4
Processes and OTP
spawn and message passing, GenServer, Agent, Task, supervision strategies and application structure. This is the part that makes Elixir different — do not skip to Phoenix before it makes sense.
Build this: Build a supervised cache as a GenServer, then kill it in IEx and watch it recover.
- 4
Months 2–3
Phoenix
Routing, controllers, contexts, Ecto for the database, channels for real-time, and LiveView. Learn Ecto's changesets properly — they are how validation and data casting work throughout.
Build this: Build a LiveView application with authentication and a real-time updating list.
- 5
Ongoing
Production and depth
Releases and hot deployment, Telemetry for instrumentation, the observer for inspecting a live system, ETS for shared in-memory state, and Dialyzer or the new type system.
Build this: Attach to a running production node with a remote console and inspect a live process's state.
Ecosystem and tooling
The tools you will end up installing whichever project you join.
| Tool | Category | What it does |
|---|---|---|
| Phoenix | Framework | The web framework — HTTP, channels, presence and LiveView |
| Ecto | Data | Database wrapper and query language; changesets handle validation and casting |
| Mix + Hex | Tooling | Build tool and package manager, included with the language |
| LiveView | Front end | Server-rendered interactive UI over WebSockets, no client framework needed |
| Oban | Jobs | Background jobs backed by PostgreSQL, with retries and scheduling |
| Broadway | Data | Data ingestion pipelines with backpressure over SQS, Kafka and RabbitMQ |
| Nx / Axon | ML | Numerical computing and neural networks on the BEAM, with GPU backends |
| Credo + Dialyzer | Code quality | Static analysis for style and for type inconsistencies |
Elixir libraries
Library coverage for Elixir is on the way.
The guide above is complete. In the meantime, the catalogues for Python, Java, JavaScript, C and C++ are fully written.
Browse all librariesFrequently asked
Do I need to learn Erlang first?
No. Elixir is self-contained and its documentation is far better. You will occasionally read Erlang — the standard library is right there and some libraries are Erlang-only — but reading it is a small step once you know Elixir, and calling it is seamless.
What does 'let it crash' actually mean in practice?
That you write the happy path and let a process die on anything unexpected, because a supervisor will restart it into a known-good state within microseconds. It is not carelessness — it is a bet that a fresh, correct state is more reliable than code attempting to repair a corrupted one, and thirty years of telecoms deployment supports that bet.
Is Elixir fast?
For concurrent I/O-bound work — web requests, connections, message passing — it is excellent, with unusually flat tail latency. For single-threaded numeric computation it is slow, comparable to Ruby. Choose it for systems handling many concurrent things, not for crunching numbers.
Is LiveView a real alternative to React?
For a large class of applications, yes, and it removes the API layer and client-side state synchronisation entirely. The trade-offs are real: it requires a persistent connection, latency is visible on every interaction, and genuinely offline or heavily client-side experiences are not a fit. For CRUD applications, dashboards and internal tools it is a significant simplification.
How hard is hiring for Elixir?
Harder than for mainstream languages, but the usual approach is hiring strong developers and training them, which typically takes a few weeks. Teams report that the smaller pool skews experienced, since people generally arrive at Elixir deliberately rather than by default.
Is the lack of static types a problem at scale?
It is the most common complaint on large codebases. Dialyzer provides success typing but is slow and its error messages are famously hard to read. The gradual set-theoretic type system being added by the core team is designed to fix exactly this, and is landing incrementally across releases.


