Managing Real-Time State with Phoenix Channels and Presence
Learn how to use Phoenix Channels and the Presence module to manage real‑time user state across distributed clusters without relying on a central database.
20 Oct 2025, 18:54 UTC

The Challenge of Synchronizing Distributed State
\nBuilding a real-time application—like a collaborative document editor or a live chat—often leads to a common architectural wall: how do you track who is online and sync their state across multiple server nodes without hammering a central database?
\nIf you rely on a database to track "online" status, every heartbeat or disconnect triggers a write operation. At scale, this creates a massive bottleneck. The solution in the Phoenix framework is to move this state into the BEAM (the Erlang Virtual Machine) using Channels for communication and Presence for state tracking.
\n\nHow Phoenix Channels Handle Concurrency
\nPhoenix Channels use WebSockets to maintain a persistent connection between the client and the server. Unlike traditional HTTP requests, which are stateless, a Channel is backed by a GenServer—a lightweight process that maintains its own state and handles messages sequentially.
\nWhen a client joins a "topic" (a logical grouping like room:123), they aren't just subscribing to a stream; they are interacting with a pub‑sub system. This allows the server to push data to thousands of clients simultaneously without the clients having to poll for updates.
Distributed Tracking with Phoenix.Presence
\nWhile Channels handle the delivery of messages, Phoenix.Presence handles the knowledge of who is connected. Presence uses a Conflict‑free Replicated Data Type (CRDT). Instead of a single source of truth in a database, every node in your cluster maintains a local copy of the presence state and synchronizes it via a gossip protocol.
This means when a user connects to Node A, Node B is notified of their presence without needing to query a shared Redis instance or Postgres table. This decentralization removes the single point of failure and reduces latency.
\n\nWorked Example: Implementing a Room Presence System
\nTo implement this, you first define a Channel that integrates the Presence tracker. This example assumes you are using Phoenix 1.7+.
\n\nServer‑side Channel Implementation:
\ndefmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
# Initialize Presence for this specific application
def trackers do
MyApp.Presence
end
def join("room:" <> room_id, payload, socket) do
# Track the user in the presence system
Phoenix.Presence.track(trackers(), "room:" <> room_id, socket.id, %{user: payload["user"]})
# Send the current list of users to the joining client
presence_list = Phoenix.Presence.list(trackers(), "room:" <> room_id)
{:ok, socket}
end
def handle_leave(_payload, socket) do
# Presence automatically handles removal on disconnect,\n # but you can perform custom cleanup here
{:ok, socket}
end
end\n\nClient‑side Connection (phoenix.js):
\nRun this in your browser console or JavaScript file to join the room and listen for presence changes:
\nconst socket = new Socket("/socket", {params: {token: "user_token"}})
socket.connect()
const channel = socket.channel("room:123", {user: "Alice"})
channel.join()
// Listen for presence updates
channel.on("presence_diff", (diff) => {
console.log("Users updated:", diff)
})\n\nTrade‑offs and Scaling Limitations
\nWhile this architecture is highly performant, it introduces specific constraints:
\n- Memory Overhead: Each WebSocket connection is a BEAM process. While lightweight, 100,000 concurrent users still consume significant RAM. Monitor your BEAM process memory closely.
- Gossip Traffic: In very large clusters (dozens of nodes), the gossip protocol used by Presence can generate significant internal network traffic. If your cluster grows too large, you may need to shard your presence topics.
- Load Balancing: Because WebSockets are stateful, you cannot use simple round‑robin load balancing for every request. You must ensure your load balancer supports "sticky sessions" or properly handles the WebSocket upgrade (HTTP 101) process.
Verifying the Implementation
\nTo verify that your real‑time state is working across a cluster, you can perform a Multi‑Node Test:
\n- Start two separate instances of your application on different ports.
- Connect them using Node.connect/1 or a distributed Erlang cluster.
- Open two browser windows: connect User A to Node 1 and User B to Node 2.
- Verify that User A receives a
presence_diffevent when User B joins Node 2.
If the update does not propagate, check that your Phoenix.PubSub adapter is correctly configured for distributed mode (e.g., using the default pg2 or Redis adapter).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.