Skip to main content
Stacklane
All insights

InsightsEngineering6 min read

Why we reach for Phoenix and Elixir on realtime projects

Realtime as the substrate, not a feature flag. The BEAM was built for telephone switches and it shows up in the numbers.

Published 7 June 2026

Most teams treat realtime as a feature flag. You build the product on a normal stack, then bolt on a WebSocket layer when the customer asks for live updates. The WebSocket layer is half-broken, the rest of the codebase has no opinion about concurrency, and every realtime feature requires a senior engineer to babysit.

Phoenix and Elixir invert that. Realtime is the substrate, not a feature. The BEAM (Erlang's virtual machine) was built in the 1980s to run telephone switches: millions of concurrent connections, supervised process trees, hot code reload, fault tolerance as a first-class concern. Phoenix is what happens when a team of compiler nerds inherits that runtime and writes a web framework on top of it.

For the realtime workloads we ship most often, operator dashboards with hundreds of live data sources, collaborative document tools, multi-tenant SaaS with presence, Phoenix is the most expensive thing to not use.

What Phoenix gives you that the reflex stack doesn't

The default Node, Python, or Ruby stack treats each request as a request-response cycle. Concurrency is a library: you reach for a queue (BullMQ, Sidekiq, Celery), a pub/sub system (Redis, NATS), a WebSocket server (Socket.io, ws), and a presence tracker you write yourself. Each one adds operational surface area. Each one has its own failure mode.

Phoenix collapses those into the framework:

  • Channels. Bidirectional, multi-topic, automatic reconnect. The same shape on the wire as WebSockets, but with backpressure, broadcast, and per-channel auth as primitives.
  • PubSub. A built-in fan-out system that scales horizontally without Redis. 10,000 listeners on a single message is a `Phoenix.PubSub.broadcast/3` call.
  • Presence. The hard problem of 'who is online right now' solved with a CRDT under the hood. Cursor sharing, room membership, join/leave events: in the box.
  • OTP supervision. Every process is supervised. A websocket handler crash doesn't take down the connection pool. A buggy GenServer doesn't take down the application. The runtime restarts the broken bit.

When it wins

Three workloads where Phoenix beats every alternative we've tried.

  • Operator dashboards with high data fanout. One server pushing the same delta to 5,000 connected operators. Phoenix at idle uses about 1GB of RAM for that. The equivalent Node + Redis setup is three boxes and a coordination layer.
  • Multi-user collaborative apps. Cursor sharing, presence indicators, conflict-free updates. LiveView's stateful server-rendered model handles this without an SPA + state library combo.
  • Per-tenant durable state. A GenServer per tenant running game state, an inference queue, a billing meter, supervised, restartable, in-process. The Erlang Distribution makes this work across a cluster without code changes.

When it loses

Three patterns where we reach for something else.

  • Native-mobile-first products. Phoenix doesn't help you write the iOS app. You still need the SPA-shaped API endpoint that the mobile client consumes.
  • Anonymous public surfaces with extreme performance budgets. A high-traffic landing page, a CDN-cacheable read API. Elixir is fast, but Rust at the edge is faster, and a static site is free.
  • Teams that won't retain an Elixir maintainer. Hiring senior Elixir engineers is harder than hiring senior TypeScript engineers. If the codebase will lapse out of active care, the Erlang stack you can't read is more expensive than the Node stack you can.
Discovery Call

Want to run the numbers for your team?

30 minutes with a founder or senior engineer. We'll do the math on your actual roadmap, including when the answer is not Stacklane.

Book a Free Call