Elixir and Phoenix for High-Concurrency Applications
[ Backend Development ]

Elixir and Phoenix for High-Concurrency Applications

Learn when to switch from Rails to Elixir. Master the BEAM VM, fault tolerance, and real-time Phoenix LiveView for high-concurrency web applications.

→ featured
→ essential
→ timely
By Paul Badarau 10 min read
[ Article Content ]
Share this article:
P
Paul Badarau
Author

Elixir and Phoenix for High-Concurrency Applications

Primary Keyword: Elixir vs Ruby
Meta Description: Learn when to switch from Rails to Elixir. Master the BEAM VM, fault tolerance, and real-time Phoenix LiveView for high-concurrency web applications.


The Problem Rails Can't Solve

Your Rails app serves 10,000 users. Response times are good. The database is optimized. Your team ships features weekly.

Then you add real-time features. A chat system. Live notifications. Collaborative editing. Suddenly, Rails struggles. WebSocket connections pile up. Memory usage spikes. You add more servers, but the problem scales linearly with cost.

This is the concurrency wall, and Rails hits it around 5,000–10,000 concurrent connections.

Elixir doesn't hit this wall. It handles 100,000+ concurrent connections on a single server. Not through clever tricks, but through fundamentally different architecture.

Why this matters: We've migrated high-traffic applications from Rails to Elixir. The result: 10x more concurrent users, 75% lower infrastructure costs, and real-time features that feel instant. For products with heavy concurrency needs, Elixir is the pragmatic choice.

By the end of this guide, you'll understand:

  • Where Rails struggles with concurrency (and why).
  • How the BEAM VM handles millions of concurrent processes.
  • What fault tolerance means in practice (and why it matters).
  • How Phoenix LiveView builds real-time UIs without JavaScript.
  • When to choose Elixir over Rails (and when to stick with Rails).

The Concurrency Problem

Concurrency is "how many things your application can do at once."

For a web app, this usually means: "How many users can be connected simultaneously?"

Rails' Concurrency Model

Rails runs on Ruby, which uses threads or processes for concurrency.

Typical Rails setup:

  • Puma (threaded): 5 workers × 5 threads = 25 concurrent requests per server.
  • Unicorn (process-based): 8 workers = 8 concurrent requests per server.

Each thread or process consumes significant memory (10–50 MB). To handle 10,000 concurrent users, you need hundreds of servers.

This works for traditional request-response apps. It doesn't work for real-time apps where connections stay open (WebSockets, long-polling).

The problem:

  • Each WebSocket connection ties up a thread or process.
  • 1,000 WebSocket connections = 1,000 threads = several GB of RAM.
  • Rails isn't designed for this. It's designed for short-lived HTTP requests.

Elixir's Concurrency Model

Elixir runs on the BEAM VM (Erlang's virtual machine). The BEAM was designed for telecom systems—millions of concurrent phone calls, each with its own connection.

How BEAM handles concurrency:

  • Each connection is a lightweight process (not an OS thread).
  • Processes are cheap: ~2 KB of memory each.
  • You can run millions of processes on a single server.
  • Processes are isolated. If one crashes, others keep running.

Result:

  • 100,000 concurrent WebSocket connections = ~200 MB of RAM.
  • Elixir apps scale vertically (more connections per server) before scaling horizontally (more servers).

The BEAM VM: Built for Concurrency

The BEAM VM is the secret behind Elixir's performance. Understanding it helps you see why Elixir is different.

Lightweight Processes

In most languages, concurrency means threads. Threads are managed by the OS and are expensive (1–2 MB each).

In Elixir, concurrency means processes. Processes are managed by the BEAM and are cheap (2 KB each).

# Spawn 100,000 processes
for i <- 1..100_000 do
  spawn(fn -> :timer.sleep(10_000) end)
end

# Memory usage: ~200 MB
# CPU usage: minimal

This isn't a benchmark trick. Elixir apps routinely handle hundreds of thousands of concurrent connections.

Preemptive Scheduling

The BEAM scheduler is preemptive. Each process gets a fixed time slice (a few milliseconds). If it doesn't finish, it's paused and another process runs.

This prevents one slow process from blocking others. In Ruby, a slow request can starve other threads.

Isolated Processes

Processes don't share memory. They communicate by sending messages.

# Process A
send(pid, {:hello, "world"})

# Process B
receive do
  {:hello, msg} -> IO.puts("Received: #{msg}")
end

This isolation is powerful:

  • No race conditions (no shared mutable state).
  • No locks or mutexes (no deadlocks).
  • If a process crashes, it doesn't take down others.

Fault Tolerance: Let It Crash

Elixir's philosophy: "Let it crash."

This sounds reckless, but it's intentional. Instead of defensive programming (checking every error, handling every edge case), Elixir embraces failure and recovers from it.

Supervisors: The Safety Net

A supervisor is a process that monitors other processes. If a child process crashes, the supervisor restarts it.

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {MyApp.Worker, []}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

defmodule MyApp.Worker do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, :ok)
  end

  def init(:ok) do
    {:ok, %{}}
  end

  # If this crashes, the supervisor restarts it automatically
  def handle_cast({:divide, a, b}, state) do
    result = a / b  # Crashes if b == 0
    {:noreply, Map.put(state, :result, result)}
  end
end

If the worker crashes (dividing by zero), the supervisor restarts it. The system stays up.

Why This Matters

In Rails, a crash usually means:

  • An exception is raised.
  • You catch it (or you don't).
  • If you don't, the request fails.
  • If the error is in a background job, you retry with exponential backoff.

In Elixir:

  • An error crashes the process.
  • The supervisor restarts it.
  • Other processes keep running.
  • The system self-heals.

This is fault tolerance. Not "never fail," but "fail gracefully and recover."


Phoenix: The Web Framework

Phoenix is Elixir's answer to Rails. It's fast, real-time by default, and built on the BEAM's strengths.

Phoenix Channels: Real-Time WebSockets

Phoenix Channels are WebSocket connections that scale.

A simple chat server:

# lib/my_app_web/channels/room_channel.ex
defmodule MyAppWeb.RoomChannel do
  use Phoenix.Channel

  def join("room:lobby", _message, socket) do
    {:ok, socket}
  end

  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast!(socket, "new_msg", %{body: body})
    {:noreply, socket}
  end
end

Client (JavaScript):

import { Socket } from "phoenix";

let socket = new Socket("/socket");
socket.connect();

let channel = socket.channel("room:lobby", {});
channel.join()
  .receive("ok", () => console.log("Connected"))
  .receive("error", () => console.log("Failed to join"));

channel.on("new_msg", (msg) => {
  console.log("New message:", msg.body);
});

channel.push("new_msg", { body: "Hello, world!" });

This handles thousands of concurrent users on a single Phoenix server. The same setup in Rails would require Redis, Action Cable, and significant tuning.

Phoenix LiveView: Real-Time Without JavaScript

Phoenix LiveView is a game-changer. It builds real-time, interactive UIs without writing JavaScript.

How it works:

  1. The server renders HTML.
  2. LiveView opens a WebSocket connection.
  3. When state changes, the server sends a diff (not the whole page).
  4. The client updates the DOM.

A simple counter:

defmodule MyAppWeb.CounterLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0)}
  end

  def render(assigns) do
    ~H"""
    <div>
      <h1>Count: <%= @count %></h1>
      <button phx-click="increment">+</button>
      <button phx-click="decrement">-</button>
    </div>
    """
  end

  def handle_event("increment", _params, socket) do
    {:noreply, assign(socket, count: socket.assigns.count + 1)}
  end

  def handle_event("decrement", _params, socket) do
    {:noreply, assign(socket, count: socket.assigns.count - 1)}
  end
end

No React. No Vue. No build tools. Just Elixir, rendering HTML and updating the DOM in real time.

This is perfect for dashboards, admin panels, and interactive forms.


When to Choose Elixir Over Rails

Elixir isn't always the right choice. It's a trade-off.

Choose Elixir When:

1. You need high concurrency.

  • Real-time features (chat, notifications, live updates).
  • WebSocket-heavy apps (multiplayer games, collaboration tools).
  • Background job processing at scale (millions of jobs per hour).

2. You need fault tolerance.

  • Mission-critical systems (financial, healthcare).
  • Systems that must stay up (99.9%+ uptime).
  • Systems where restarts are expensive.

3. You need low latency.

  • Phoenix response times are consistently <5 ms.
  • Rails response times are 50–200 ms (depending on complexity).

4. You're building for the long term.

  • Elixir codebases age well. Functional patterns reduce technical debt.
  • The BEAM VM is mature (30+ years old).

Choose Rails When:

1. You're building an MVP.

  • Rails ships faster. The ecosystem is larger.
  • You don't need real-time features yet.

2. Your team knows Ruby.

  • Elixir has a learning curve (functional programming, OTP patterns).
  • Rails lets you ship today.

3. You need a rich ecosystem.

  • Rails has 15+ years of gems, guides, and community support.
  • Elixir's ecosystem is smaller but growing.

4. Your app is CRUD-heavy.

  • If your app is mostly database reads/writes, Rails is fine.
  • Elixir's strengths shine when concurrency matters.

Real-World Migration: Chat App

We migrated a chat application from Rails to Elixir. Here's what changed:

Before (Rails + Action Cable)

  • 12 servers (AWS m5.large).
  • 5,000 concurrent users.
  • Infrastructure cost: $2,400/month.
  • Message latency: 100–300 ms.

After (Phoenix Channels)

  • 2 servers (AWS m5.large).
  • 50,000 concurrent users.
  • Infrastructure cost: $400/month.
  • Message latency: 10–20 ms.

The difference:

  • Rails needed 1 server per 400–500 concurrent WebSocket connections.
  • Phoenix handled 25,000 connections per server.

This isn't a criticism of Rails. Rails wasn't designed for this. Elixir was.


Learning Elixir: The Functional Shift

Elixir is a functional language. If you're coming from Ruby (object-oriented), this is a shift.

Immutability

In Ruby, you mutate data:

user = { name: "Alice" }
user[:email] = "alice@example.com"

In Elixir, data is immutable:

user = %{name: "Alice"}
user = Map.put(user, :email, "alice@example.com")
# Original user is unchanged; a new map is returned

This feels awkward at first. But it eliminates bugs. You can't accidentally mutate shared state.

Pattern Matching

Elixir uses pattern matching everywhere:

defmodule Math do
  def factorial(0), do: 1
  def factorial(n), do: n * factorial(n - 1)
end

Math.factorial(5)  # => 120

This is clearer than conditionals:

def factorial(n)
  return 1 if n == 0
  n * factorial(n - 1)
end

Pipelines

Elixir's pipe operator (|>) chains transformations:

"hello world"
|> String.upcase()
|> String.split()
|> Enum.map(&String.reverse/1)
|> Enum.join(" ")
# => "OLLEH DLROW"

This reads left-to-right, like Ruby's method chaining.


Phoenix vs. Rails: A Feature Comparison

Feature Rails Phoenix
Request speed 50–200 ms 5–20 ms
Concurrency 100–1,000 connections/server 10,000–100,000 connections/server
Real-time Action Cable (requires Redis) Channels (built-in)
Ecosystem Huge (15+ years) Growing (10 years)
Learning curve Moderate Steep (functional programming)
Fault tolerance Manual (rescue, retries) Built-in (supervisors)
Deployment Heroku, AWS, Docker Fly.io, Gigalixir, Docker
Type safety No (dynamic) Optional (Dialyzer)

Bringing It All Together

Elixir and Phoenix are the right choice when concurrency, fault tolerance, and low latency matter.

Rails is the right choice when shipping speed, ecosystem, and familiarity matter.

Most apps start with Rails. When they hit the concurrency wall, they migrate critical pieces to Elixir. Chat systems, notification services, background job processors—these are natural fits.

You don't have to rewrite everything. Use Rails for CRUD. Use Elixir for real-time. Both can coexist.

If you're building something real-time, give Phoenix a try. If you're building an MVP, start with Rails. Know the trade-offs, and choose intentionally.

Have you used Elixir in production? What worked? What didn't? Share on Twitter or LinkedIn—we'd love to hear your experience.


Further Reading

[ Let's Build Together ]

Ready to transform your
business with software?

From strategy to implementation, we craft digital products that drive real business outcomes. Let's discuss your vision.

Related Topics:
Elixir Phoenix Rails concurrency BEAM real-time 2025