Managing State Synchronization in Phoenix Channels with PubSub
Learn how to synchronize state across distributed Phoenix nodes using PubSub to build scalable, real-time bidirectional communication.
07 Jul 2026, 13:40 UTC

The Challenge of Distributed Real-Time State
When building a real-time application—like a collaborative document or a live dashboard—the primary technical hurdle isn't establishing a WebSocket connection; it's ensuring that when User A performs an action, User B sees that change instantly, even if they are connected to different server nodes in a cluster.
The core problem is that Phoenix Channels are stateful processes. Each connected client has its own dedicated Elixir process on the server. Without a synchronization layer, a message sent to one process is invisible to all others. To solve this, Phoenix uses Phoenix.PubSub to decouple the event source from the delivery mechanism.
How PubSub Bridges Isolated Processes
Phoenix.PubSub acts as a distribution hub. Instead of a process trying to find and message other specific user processes, it broadcasts a message to a topic (a unique string identifier). Any process that has "subscribed" to that topic will receive the message and can then push it down the WebSocket to the client.
This architecture leverages the BEAM (the Erlang Virtual Machine) concurrency model. Because each channel is a lightweight process, the server can handle thousands of concurrent connections. If one process crashes due to a malformed client request, the PubSub system and other user processes remain unaffected.
Implementing a Synchronized Room
To implement this, you need a Channel module that handles the initial connection (the join) and a mechanism to broadcast updates. In this example, we assume a "room" based system where users share a room_id.
# lib/my_app_web/channels/room_channel.ex
defmodule MyAppWeb.RoomChannel do
use MyAppWeb.Channel
# The join callback authenticates the user and subscribes them to a topic
def join("room:room_id", socket) do
# In a real app, verify the user's permission to join room_id here
{:ok, socket}
end
# Handle incoming messages from the client
def handle_in("new_msg", payload, socket) do
# Broadcast the message to everyone subscribed to this specific room
Phoenix.PubSub.broadcast(
MyApp.PubSub,
"room:" <> payload["room_id"],
"new_msg",
payload
)
{:reply, {:ok, "message_sent"}, socket}
end
end
Execution Details:
- Where to run: This code resides in the server-side Elixir application.
- Permissions: The process must have access to the
MyApp.PubSubsupervisor defined in yourapplication.ex. - Expected Check: Use the browser console to verify the client-side listener:
socket.channel("room:123").join().push("new_msg", {room_id: "123", text: "Hello"}). Other clients joined toroom:123should trigger their.on("new_msg", ... )callback.
Scaling Across Nodes
By default, Phoenix PubSub uses Distributed Erlang (pg), meaning nodes communicate directly over the network. This is highly efficient for small to medium clusters. However, as you scale to dozens of nodes, the overhead of maintaining a full mesh network can increase.
| Mechanism | Best For | Trade-off |
|---|---|---|
| Native Distributed Erlang | Low-latency, small clusters | High node-to-node connection overhead |
| Redis Adapter | Large, dynamic clusters | Additional infrastructure dependency; slight latency increase |
Critical Limitations and Risks
While PubSub is powerful, it introduces two primary risks: Memory Exhaustion and the Thundering Herd effect.
Memory scales linearly with the number of open WebSockets. If you store large amounts of state inside the socket assigns, you can quickly deplete server RAM. Always keep the socket state minimal and fetch heavy data from a database or cache.
The "Thundering Herd" occurs when a single broadcast is sent to tens of thousands of users simultaneously. The server must suddenly serialize and push thousands of packets, which can spike CPU and cause network congestion. To mitigate this, implement server-side throttling or break large groups into smaller, tiered topics.
Verification and Validation
To verify your implementation is working across a distributed environment, run two separate instances of your application on different ports. Connect Client A to Port 4000 and Client B to Port 4001. If Client B receives a message broadcast by Client A, your PubSub cluster is correctly configured.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.