Phoenix Framework
Phoenix is Elixir's web framework, and its distinguishing characteristics come from the runtime rather than the framework. The BEAM gives every request its own lightweight process with its own heap, preemptively scheduled, isolated from every other request — which means a crash affects one request, garbage collection never pauses the whole system, and a single machine holding a million open WebSocket connections is a demonstrated result rather than a marketing claim.
That foundation makes two things practical that are awkward elsewhere. Channels give you stateful, soft-real-time connections at a scale where a thread-per-connection or event-loop model struggles. And LiveView — the framework's most distinctive feature — renders interactive UI on the server, diffing the rendered output and pushing minimal patches over a WebSocket, so a substantial class of application achieves SPA-like interactivity with no client-side state management and very little JavaScript.
Phoenix's other notable design choice is contexts: an explicit convention that pushes business logic out of controllers into named domain modules. It's a modest amount of structure that pays off substantially as an application grows.
TL;DR
- Built on the BEAM: a process per request, preemptive scheduling, isolated heaps, no stop-the-world GC.
- Plug is the composable middleware abstraction — everything in the request pipeline is a plug.
- Contexts are the convention that keeps business logic out of controllers. Use them.
- Ecto is not an ORM — it's explicit query building plus changesets for validation.
- Channels give bidirectional WebSocket messaging with topic subscriptions and presence tracking.
- LiveView renders on the server and pushes DOM diffs. Interactive UI, minimal JavaScript.
- PubSub is built in and cluster-aware — broadcasts reach subscribers on any node.
- Excellent for real-time and high-connection-count workloads; a smaller ecosystem than Node or Rails.
Quick Example
A context, a controller, and a LiveView over the same domain:
That LiveView has no API endpoint, no client-side state, no fetch call, and no JSON serialization — and it updates in real time when any other user creates a product. The search input round-trips to the server on each keystroke (debounced), which sounds alarming and works because the round trip is a small diff over an already-open WebSocket.
Core Concepts
The request pipeline
Plug is the whole abstraction: a function taking a connection and options and returning a connection. Middleware, controllers, and the endpoint itself are all plugs, which makes the pipeline uniform and composable — and makes writing your own middleware a fifteen-line module.
Contexts
The rule is that the web layer calls context functions and never touches schemas or Repo directly. That gives you a domain boundary you can test without HTTP, reuse from a channel or a background job, and reason about independently. It's a lighter-weight version of the same instinct behind hexagonal architecture, enforced by convention rather than by interfaces.
Phoenix generators create contexts by default, which is why the convention actually gets followed.
Ecto
Ecto is deliberately not an ORM — no lazy loading, no implicit N+1, no object identity map:
Ecto.Multi is genuinely excellent: a transaction as a composable data structure, where a failure tells you exactly which named step failed. Changesets separate validation from persistence, so you can validate a form without a database and surface both application-level and database-constraint errors through one interface.
Channels and PubSub
Each channel is a separate BEAM process, so one misbehaving connection cannot affect others. PubSub is cluster-aware by default — a broadcast on one node reaches subscribers on every node, with no external broker required. Presence implements a CRDT-based distributed presence set, which is a genuinely hard problem solved in the framework.
LiveView
The tradeoffs are real and worth stating. It requires a persistent connection, so offline behavior and flaky networks need handling (phx-disconnected styling helps). Every interaction is a round trip, so latency is visible — LiveView applications feel noticeably better when served close to users. And each connection holds server memory and a process, so capacity planning is different from a stateless API.
Where it excels: dashboards, admin interfaces, forms, collaborative features, and anything data-heavy where the alternative is building an API plus a client-side state layer to consume it. For interactions needing genuinely zero latency — drag and drop, canvas, animation — you use a JavaScript hook, which LiveView supports explicitly.
Phoenix vs. the Alternatives
Phoenix wins clearly on real-time features, connection counts, latency consistency under load, and fault isolation. It loses on ecosystem breadth and hiring. The functional paradigm and OTP concepts are a genuine ramp for developers coming from object-oriented backgrounds — usually a few weeks to productive, longer to comfortable with supervision trees and process design.
The comparison that matters most in practice: for a real-time or high-connection application, Phoenix does with the framework what other stacks do with additional infrastructure. That's a meaningful reduction in moving parts.
Best Practices
Keep business logic in contexts
Controllers and LiveViews handle HTTP and UI concerns; contexts own the domain. A controller that builds Ecto queries or calls Repo directly has put domain logic in the web layer, and it will be duplicated the first time a background job needs the same operation.
Preload explicitly and check for N+1
Ecto has no lazy loading, which means N+1 is a visible mistake rather than a hidden one — but only if you look. preload: in the query, and enable query logging in development to see the count.
Use Ecto.Multi for multi-step writes
Named steps, one transaction, and an error that identifies exactly which step failed. It's clearer than nested case statements inside Repo.transaction/1 and composable across contexts.
Let it crash, and supervise properly
Don't wrap everything in defensive try/rescue. A crashed process is restarted by its supervisor in a known-good state, which is more reliable than continuing in a state you didn't anticipate. Reserve explicit error handling for expected failures — a validation error, a 404 — and let genuinely unexpected states crash.
Assign only what the template needs in LiveView
Every assign is held in the LiveView process's memory for the connection's lifetime, and large assigns multiplied by thousands of connections is real memory. Use streams for collections (phx-update="stream"), and temporary_assigns for data you render once.
Use streams for large or append-heavy lists
stream/4 avoids keeping the full collection in socket state, sending only inserts, updates, and deletes. For a chat log, a feed, or a table of thousands of rows, this is the difference between viable and not.
Reach for a JavaScript hook when the interaction needs it
LiveView isn't all-or-nothing. phx-hook gives you a JavaScript escape hatch for drag-and-drop, charts, maps, and anything needing zero-latency local response. Fighting to express those server-side is the wrong instinct.
Run Dialyzer and Credo in CI
Elixir is dynamically typed, and Dialyzer's success typing catches a meaningful class of error from type specs. Credo enforces consistency and catches common mistakes. Both are standard in mature Elixir projects. See Linting & Formatting.
Common Mistakes
Business logic in controllers
Forgetting to preload
Unbounded LiveView assigns
Defensive error handling everywhere
Blocking the LiveView process
Treating Ecto like ActiveRecord
FAQ
Is Phoenix worth learning if I already know Rails or Node?
If you build real-time features, need high connection counts, or care about latency consistency under load, yes — it does things with the framework that other stacks need extra infrastructure for. The concurrency model and fault isolation are genuinely different rather than incrementally better. The costs are a smaller ecosystem, a smaller hiring pool, and a functional paradigm that takes a few weeks to become productive in. See Elixir.
Is LiveView a real alternative to React?
For a large class of applications, yes. Dashboards, admin panels, forms, CRUD interfaces, and collaborative features are frequently better served by LiveView — no API to build, no client state to synchronize, no serialization layer. Where it isn't the answer: offline-capable applications, interactions needing zero-latency local response, complex client-side computation, and native mobile clients (though LiveView Native is developing). It also requires users to have a working connection, which is fine for internal tools and a consideration for consumer apps on poor networks.
How does it handle a million connections?
BEAM processes are a few kilobytes each, preemptively scheduled by the runtime across all cores, with per-process heaps so garbage collection is per-process and never stops the world. A WebSocket connection is a process. The widely cited two-million-connections benchmark required kernel tuning and a large machine, and the ordinary result — hundreds of thousands of connections on commodity hardware — is what makes the architecture practical.
What's the deployment story?
mix release produces a self-contained release including the Erlang runtime, deployable as a binary or in a small container image. Fly.io has particularly good Elixir support including clustering, and Gigalixir is Elixir-specific. Standard containers on Kubernetes work fine, with the note that BEAM clustering (for distributed PubSub and Presence) needs node discovery — libcluster handles this with a Kubernetes strategy.
Is Ecto harder than an ORM?
More explicit, and easier once you adjust. There's no lazy loading, so you say what to preload; no implicit N+1, because associations don't silently query; and changesets separate validation from persistence rather than mixing them into model callbacks. The learning curve is real if you're used to ActiveRecord's magic, and the payoff is that a query's cost is visible in the code that writes it.
What about the smaller ecosystem?
It's a genuine consideration and less limiting than the raw package count suggests. The essentials are excellent and well-maintained: Ecto, Oban (background jobs), Absinthe (GraphQL), Broadway (data pipelines), Bandit (HTTP server), Req (HTTP client). Where you'll notice the gap is niche integrations and SDKs — some vendors ship no Elixir client, and you write a thin wrapper over their REST API. Erlang interop also means the entire Erlang ecosystem is available.
Related Topics
- Elixir — The language, BEAM processes, and OTP supervision
- WebSockets — What channels and LiveView run over
- Server-Sent Events — The simpler one-way alternative
- Ruby on Rails — The framework Phoenix drew conventions from
- Hexagonal Architecture — Contexts as a lighter version of the same idea
- PostgreSQL — Ecto's best-supported database
- GraphQL — Absinthe for Phoenix
- Object-Relational Mapping — What Ecto deliberately isn't
- Backend Development — Where Phoenix fits among frameworks