Skip to content
Elixir logo

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

Pattern matching is the control flow
# `=` 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"
Multiple function clauses replace the branching most languages put inside a function body. The dispatch happens on the argument's structure, which keeps each clause small and makes an unhandled case a loud FunctionClauseError rather than a silent fallthrough.
Processes and message passing
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}
end
State lives inside a process and is reached only by sending messages, so there are no locks and no shared mutable memory. `call` blocks for a reply and has a five-second default timeout; `cast` returns immediately and gives no delivery guarantee.
Supervision — let it crash
defmodule 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
The counter can crash on a malformed message and the supervisor restarts it with a clean initial state within microseconds — the web endpoint never notices. This is why Elixir code contains so few defensive checks: recovery is structural rather than written into every function.
Concurrent work with Task
# 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}
end
`async_stream` bounds concurrency, so ten thousand inputs do not open ten thousand connections. `with` flattens what would otherwise be deeply nested case expressions, and the else block handles every failure in one place.

Common 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. 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. 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. 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. 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. 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.

ToolWhat it does
PhoenixThe web framework — HTTP, channels, presence and LiveView
EctoDatabase wrapper and query language; changesets handle validation and casting
Mix + HexBuild tool and package manager, included with the language
LiveViewServer-rendered interactive UI over WebSockets, no client framework needed
ObanBackground jobs backed by PostgreSQL, with retries and scheduling
BroadwayData ingestion pipelines with backpressure over SQS, Kafka and RabbitMQ
Nx / AxonNumerical computing and neural networks on the BEAM, with GPU backends
Credo + DialyzerStatic 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 libraries

Frequently 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.