Building Real-Time Applications with Elixir and Phoenix: A Practical Guide
In today’s digital landscape, users expect instant feedback and real-time interactions. Whether it’s a chat application, collaborative document editing, live dashboards, or multiplayer games, real-time features have become table stakes for modern web applications.
While many frameworks struggle to implement real-time functionality efficiently, Elixir and Phoenix make it feel effortless. Built on the battle-tested Erlang VM (BEAM), Elixir was designed from the ground up for exactly this kind of challenge: handling thousands of concurrent connections with minimal latency and maximum reliability.
In this guide, we’ll explore how to build production-ready real-time applications with Elixir and Phoenix, and why this technology stack is revolutionizing how developers approach real-time features.
Why Elixir Excels at Real-Time Applications
Before diving into implementation details, let’s understand what makes Elixir uniquely suited for real-time applications.
Lightweight Processes and Massive Concurrency
Elixir runs on the BEAM VM, which uses lightweight processes (not OS threads) that are incredibly cheap to create and manage. Each process:
- Uses only 2-3KB of memory initially
- Can grow and shrink dynamically
- Runs in complete isolation
- Communicates via message passing
What this means in practice: You can easily run millions of processes on a single server. Each WebSocket connection, chat room, or user session can have its own dedicated process without worrying about resource exhaustion.
Real-world impact:
- Discord handles 5+ million concurrent users using Elixir
- WhatsApp (built on Erlang, Elixir’s foundation) supported 900 million users with just 50 engineers
- Phoenix can handle 2+ million WebSocket connections on a single server
Built-In Fault Tolerance
Real-time applications need to be resilient. Network issues, unexpected errors, and edge cases are inevitable when dealing with thousands of concurrent connections.
Elixir’s “let it crash” philosophy combined with supervision trees means:
- Failed processes restart automatically
- Errors are isolated and don’t cascade
- Your system self-heals without manual intervention
- Users experience minimal disruption
Low Latency and High Throughput
The BEAM VM’s scheduler ensures fair distribution of work across CPU cores, and its garbage collector is optimized for low-latency scenarios. This translates to:
- Sub-millisecond message passing between processes
- Consistent performance under heavy load
- No stop-the-world GC pauses affecting user experience
Phoenix: The Real-Time Web Framework
Phoenix is to Elixir what Rails is to Ruby—but with performance that leaves traditional frameworks in the dust. Phoenix provides two powerful tools for building real-time features:
1. Phoenix Channels (WebSockets Made Simple)
Phoenix Channels provide a simple, powerful abstraction over WebSockets for bidirectional communication.
Basic Channel Example:
# lib/my_app_web/channels/room_channel.ex
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
@impl true
def join("room:" <> room_id, _params, socket) do
# Authorize and join the room
{:ok, assign(socket, :room_id, room_id)}
end
@impl true
def handle_in("new_message", %{"body" => body}, socket) do
# Broadcast message to everyone in the room
broadcast!(socket, "new_message", %{
body: body,
user: socket.assigns.user_id,
timestamp: DateTime.utc_now()
})
{:noreply, socket}
end
@impl true
def handle_in("typing", _params, socket) do
# Broadcast typing indicator to others
broadcast_from!(socket, "user_typing", %{
user: socket.assigns.user_id
})
{:noreply, socket}
end
end
Client-Side JavaScript:
import {Socket} from "phoenix"
let socket = new Socket("/socket", {
params: {token: userToken}
})
socket.connect()
let channel = socket.channel("room:lobby", {})
channel.on("new_message", payload => {
renderMessage(payload)
})
channel.on("user_typing", payload => {
showTypingIndicator(payload.user)
})
// Send a message
document.querySelector("#send-btn").addEventListener("click", () => {
channel.push("new_message", {
body: document.querySelector("#message-input").value
})
})
// Join the channel
channel.join()
.receive("ok", resp => console.log("Joined successfully"))
.receive("error", resp => console.log("Unable to join", resp))
What you get for free:
- Automatic reconnection with exponential backoff
- Presence tracking (see who’s online)
- Message delivery guarantees
- Support for multiple transports (WebSocket, long polling fallback)
- Built-in authentication
2. Phoenix LiveView (Real-Time Without JavaScript)
Phoenix LiveView is revolutionary: build rich, real-time user experiences without writing JavaScript. The server pushes DOM updates over WebSocket, and LiveView applies them efficiently.
Simple Counter Example:
# lib/my_app_web/live/counter_live.ex
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
@impl true
def render(assigns) do
~H"""
<div>
<h1>Count: <%= @count %></h1>
<button phx-click="increment">+</button>
<button phx-click="decrement">-</button>
</div>
"""
end
@impl true
def handle_event("increment", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count + 1)}
end
@impl true
def handle_event("decrement", _params, socket) do
{:noreply, assign(socket, :count, socket.assigns.count - 1)}
end
end
That’s it! You now have a real-time, interactive UI with automatic state synchronization between client and server. No Redux, no API endpoints, no JavaScript state management.
Advanced LiveView Example: Real-Time Dashboard:
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
# Subscribe to real-time updates
Phoenix.PubSub.subscribe(MyApp.PubSub, "metrics")
:timer.send_interval(1000, self(), :update_metrics)
end
{:ok, assign(socket,
active_users: 0,
requests_per_second: 0,
avg_response_time: 0
)}
end
@impl true
def render(assigns) do
~H"""
<div class="dashboard">
<div class="metric-card">
<h3>Active Users</h3>
<p class="metric-value"><%= @active_users %></p>
</div>
<div class="metric-card">
<h3>Requests/sec</h3>
<p class="metric-value"><%= @requests_per_second %></p>
</div>
<div class="metric-card">
<h3>Avg Response Time</h3>
<p class="metric-value"><%= @avg_response_time %>ms</p>
</div>
</div>
"""
end
@impl true
def handle_info(:update_metrics, socket) do
metrics = MetricsCollector.get_current_metrics()
{:noreply, assign(socket,
active_users: metrics.active_users,
requests_per_second: metrics.rps,
avg_response_time: metrics.avg_response_time
)}
end
@impl true
def handle_info({:metric_update, metric, value}, socket) do
{:noreply, assign(socket, metric, value)}
end
end
The dashboard automatically updates in real-time without any JavaScript fetch calls or polling!
Real-World Use Cases
1. Chat Applications
Perfect for Elixir’s strengths: many concurrent connections, message broadcasting, presence tracking.
Implementation approach:
- One process per chat room
- Phoenix Channels for message delivery
- Phoenix Presence for “who’s online” tracking
- PubSub for broadcasting messages across server nodes
Benefits:
- Handle millions of concurrent users
- Sub-millisecond message latency
- Built-in typing indicators and read receipts
- Scales horizontally across multiple servers
Think Google Docs-style collaboration: multiple users editing the same document simultaneously.
Implementation approach:
- LiveView for real-time UI updates
- Operational transformation or CRDT for conflict resolution
- Phoenix Channels for broadcasting changes
- Process per document for state management
Benefits:
- Real-time cursor positions
- Live editing without conflicts
- Automatic synchronization
- No complex client-side state management
3. Live Dashboards and Analytics
Display real-time metrics, monitoring data, or business analytics.
Implementation approach:
- LiveView for the dashboard UI
- GenStage or Broadway for data processing pipelines
- PubSub for distributing updates
- Telemetry for metrics collection
Benefits:
- Update dashboards automatically as data changes
- No polling required
- Efficient data pipelines
- Built-in backpressure handling
4. Multiplayer Games
Turn-based or real-time multiplayer gaming experiences.
Implementation approach:
- One process per game instance
- Phoenix Channels for player communication
- Presence for tracking active players
- ETS for fast in-memory game state
Benefits:
- Low-latency game state updates
- Fault-tolerant game servers
- Easy horizontal scaling
- Built-in player session management
5. Live Notifications and Feeds
Real-time activity feeds, notifications, or news updates.
Implementation approach:
- LiveView for the feed UI
- PubSub for event distribution
- Phoenix Channels for targeted notifications
- Process registry for user sessions
Benefits:
- Instant notification delivery
- No polling overhead
- Efficient resource usage
- Scalable to millions of users
Advanced Patterns and Best Practices
Presence Tracking
Phoenix Presence makes it trivial to track who’s online:
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
alias MyAppWeb.Presence
def join("room:" <> room_id, _params, socket) do
send(self(), :after_join)
{:ok, assign(socket, :room_id, room_id)}
end
def handle_info(:after_join, socket) do
# Track this user's presence
{:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
online_at: inspect(System.system_time(:second)),
username: socket.assigns.username
})
# Send current presence list to user
push(socket, "presence_state", Presence.list(socket))
{:noreply, socket}
end
end
Client-side:
let presences = {}
channel.on("presence_state", state => {
presences = Presence.syncState(presences, state)
renderUsers(presences)
})
channel.on("presence_diff", diff => {
presences = Presence.syncDiff(presences, diff)
renderUsers(presences)
})
Distributed Real-Time Applications
Elixir makes it easy to scale across multiple servers:
# lib/my_app/application.ex
def start(_type, _args) do
children = [
# ...other supervisors
{Phoenix.PubSub, name: MyApp.PubSub},
MyAppWeb.Endpoint,
{Cluster.Supervisor, [topologies(), [name: MyApp.ClusterSupervisor]]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
defp topologies do
[
my_app: [
strategy: Cluster.Strategy.Kubernetes.DNS,
config: [
service: "my-app-service",
application_name: "my_app"
]
]
]
end
With just this configuration, your Phoenix application automatically:
- Discovers other nodes in the cluster
- Distributes PubSub messages across all nodes
- Routes Channel messages to the correct server
- Maintains Presence state across the cluster
No manual configuration needed!
Rate Limiting and Backpressure
Protect your real-time features from abuse:
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
# Rate limit: 10 messages per 10 seconds
@rate_limit_window 10_000
@max_messages 10
def handle_in("new_message", params, socket) do
case check_rate_limit(socket) do
:ok ->
process_message(params, socket)
{:error, :rate_limited} ->
{:reply, {:error, %{reason: "Rate limited. Slow down!"}}, socket}
end
end
defp check_rate_limit(socket) do
now = System.system_time(:millisecond)
messages = socket.assigns[:recent_messages] || []
# Filter messages within the time window
recent = Enum.filter(messages, fn ts -> now - ts < @rate_limit_window end)
if length(recent) < @max_messages do
:ok
else
{:error, :rate_limited}
end
end
end
1. Use Binary Protocols
For high-throughput scenarios, consider using MessagePack or Protocol Buffers instead of JSON:
# config/config.exs
config :phoenix, :format_encoders,
msgpack: MessagePack
# Channel with binary encoding
socket "/socket", MyAppWeb.UserSocket,
websocket: [
serializer: [{Phoenix.Socket.V2.MessagePackSerializer, "~> 2.0.0"}]
]
2. Optimize LiveView Updates
Only send what changed:
def handle_info(:update, socket) do
# ❌ Bad: Re-assign everything
# {:noreply, assign(socket, :data, fetch_all_data())}
# ✅ Good: Only update what changed
new_items = fetch_new_items(socket.assigns.last_id)
{:noreply, update(socket, :items, &(&1 ++ new_items))}
end
3. Use Stream Efficiently
For large lists, use streams in LiveView:
def render(assigns) do
~H"""
<div id="items" phx-update="stream">
<div :for={{dom_id, item} <- @streams.items} id={dom_id}>
<%= item.name %>
</div>
</div>
"""
end
Reduce server load from rapid events:
<input
type="text"
phx-keyup="search"
phx-debounce="300"
value={@query}
/>
Getting Started: Your First Real-Time App
Ready to build your first real-time application with Elixir? Here’s a quick roadmap:
Step 1: Install Elixir and Phoenix
# Install Elixir (via asdf, homebrew, or your package manager)
asdf install elixir latest
# Install Phoenix
mix archive.install hex phx_new
Step 2: Create a New Phoenix Project
mix phx.new my_app --live
cd my_app
# Set up database
mix ecto.create
# Start the server
mix phx.server
Visit http://localhost:4000 and you’ll see your Phoenix app running!
Step 3: Add a Real-Time Feature
Generate a LiveView:
mix phx.gen.live Chats Message messages content:text user:string
Update your router:
# lib/my_app_web/router.ex
live "/messages", MessageLive.Index, :index
live "/messages/new", MessageLive.Index, :new
Run migrations:
mix ecto.migrate
You now have a real-time message board!
Step 4: Deploy to Production
Phoenix apps are easy to deploy:
Using Fly.io (recommended):
fly launch
fly deploy
Using Docker:
FROM elixir:1.15-alpine AS build
# ... build steps
FROM alpine:3.18
# ... runtime setup
When to Choose Elixir for Real-Time Features
Elixir is an excellent choice when you need:
✅ Perfect fit:
- Millions of concurrent WebSocket connections
- Real-time chat, collaboration, or gaming
- Live dashboards and monitoring
- Event streaming and processing
- High-availability requirements
- Soft real-time guarantees
⚠️ Consider alternatives:
- CPU-intensive calculations (consider Rust or C++ for hot paths)
- Existing large team with no Elixir experience (training investment required)
- Simple CRUD apps with minimal real-time needs (any framework works)
How Async Squad Labs Can Accelerate Your Real-Time Journey
Building real-time features is one thing—building them at scale, with proper architecture, monitoring, and maintenance, is another challenge entirely.
At Async Squad Labs, we specialize in architecting and implementing production-grade real-time applications with Elixir and Phoenix. Our team has deep expertise in:
Real-Time System Architecture
- Designing scalable WebSocket infrastructure
- Implementing efficient message broadcasting strategies
- Building fault-tolerant distributed systems
- Optimizing for low latency and high throughput
Phoenix Expertise
- LiveView applications that feel native
- Complex Channel implementations with custom protocols
- Presence tracking at scale
- Performance optimization and load testing
Full-Stack Implementation
- From initial architecture to production deployment
- Integration with existing systems and APIs
- Database design for real-time workloads
- Monitoring and observability setup
Team Training and Support
- Onboarding your team to Elixir and Phoenix
- Code reviews and best practices mentoring
- Knowledge transfer and documentation
- Ongoing support and maintenance
Whether you’re:
- Starting from scratch: We’ll architect and build your real-time application from the ground up
- Migrating from another platform: We’ll help you transition to Elixir smoothly
- Scaling existing features: We’ll optimize your current implementation for growth
- Adding real-time capabilities: We’ll integrate LiveView or Channels into your existing app
We bring the expertise to make your real-time project a success.
Conclusion: The Future is Real-Time
Real-time features are no longer optional—they’re expected. Users demand instant feedback, live updates, and seamless collaboration. The question isn’t whether to build real-time features, but how to build them efficiently, reliably, and at scale.
Elixir and Phoenix provide the answer. With unmatched concurrency, built-in fault tolerance, and developer-friendly abstractions like LiveView and Channels, they make building real-time applications not just possible, but genuinely enjoyable.
The technology that powers Discord’s millions of concurrent users, WhatsApp’s 900 million connections, and Pinterest’s real-time notifications is available to you today.
The tools are mature, the community is vibrant, and the success stories speak for themselves. Whether you’re building a chat application, collaborative tool, live dashboard, or the next big real-time platform, Elixir gives you the foundation to succeed.
Stop fighting your framework. Start building real-time features the way they were meant to be built.
Ready to bring your real-time vision to life? Contact Async Squad Labs to discuss how we can help architect and build your next real-time application with Elixir and Phoenix.
Learn more about Elixir: Why Elixir? | Our Services | Get Started
Our team of experienced software engineers specializes in building scalable applications with Elixir, Python, Go, and modern AI technologies. We help companies ship better software faster.
📬 Stay Updated with Our Latest Insights
Get expert tips on software development, AI integration, and best practices delivered to your inbox. Join our community of developers and tech leaders.