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

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:

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

References