Matrjoschka of phoenix communication
Phoenix, the powerful Elixir web framework, is renowned for its real-time capabilities. But with terms like Channels, Presence, and PubSub floating around, it’s easy for newcomers to feel overwhelmed. What exactly do these components do, and how do they relate to each other?
If you’ve found yourself puzzling over the differences between Phoenix Channels and PubSub, or wondering when to use which feature, you’re not alone. In this post, we’ll demystify Phoenix’s real-time toolbox, breaking down each component and its role in building dynamic, responsive applications.
From the foundational PubSub server to the high-level abstractions of Channels and Presence, we’ll explore how these layers work together and when to leverage each one. By the end of this guide, you’ll have a clear understanding of Phoenix’s real-time architecture, empowering you to choose the right tools for your next project.
Smallest Building Block: Registry
Before we dive into Phoenix’s real-time features, let’s start with a fundamental
building block: the Registry
module. You might wonder why we’re beginning with
an Elixir module in a post about Phoenix. The answer lies in the evolution of
Phoenix’s PubSub system.
The Registry
module was added to Elixir 1.4
(2016) after being extracted and extended
based on code within the Phoenix.PubSub
codebase. This was right after José and Chris
had worked on scaling Phoenix to for the famous 2 Million Websocket Connections
benchmark.
Interestingly, it took almost three more years for phoenix_pubsub to actually
adopt Registry
in its 2.0 version.
With the history out of the way – what is Registry
for?
Registry
provides local process registration on a beam node. One part of that
is the keys: :duplicate
setting, which allows many processes to register under
a single key. Using Registry.dispatch
those registrations can be used to
implement node local PubSub.
Building on Registry: Phoenix.PubSub
After understanding the local capabilities of Registry
, let’s explore how Phoenix
scales this concept across multiple nodes with Phoenix.PubSub
. While Registry
excels at managing processes within a single node, Phoenix.PubSub
extends this
functionality to enable cluster-wide communication in distributed Phoenix applications.
Phoenix.PubSub
acts as a bridge between nodes, allowing Registry
instances on
individual nodes to forward messages to local subscribers – while also sharing
broadcasted messages with the rest of the cluster. This is crucial for applications
that need to scale horizontally across multiple servers. Since version 2.0,
Phoenix.PubSub
is using OTP’s process groups (:pg
) module for its
distributed functionality.
Registry
and Phoenix.PubSub
form the backbone of Phoenix’s real-time features.
Including external clients: Phoenix.Channel
With Phoenix.Channel
s the scope of the PubSub communication is extended again,
this time adding external clients into the mix. Typically, these external clients
are users of Phoenix websites, with their browsers establishing connections to
Elixir nodes via WebSockets or long-polling.
However, the power of Channels isn’t limited to web browsers. The Channel abstraction is protocol-agnostic, meaning it can be implemented on top of various transport protocols. This flexibility allows Phoenix applications to communicate with a wide array of clients - mobile apps, IoT devices, or even other server applications. Any client able of speaking the chosen transport protocol can participate in the real-time communication.
One good example in the IoT / other server space is NervesHub, which implements remote IEx shells for embedded devices, as well as distributing firmware updates, over Channels.
Channels are the final layer of extension on the PubSub side of Phoenix. Next we’ll look at two related features of Phoenix, which make use of PubSub to enable more complex functionality in Phoenix.
UseCase #1: Phoenix.Presence
Phoenix.Presence
is a fairly prominent feature of Phoenix. But keeping with
the theme of going from low-level to high-level abstraction Phoenix.Tracker
must be mentioned first.
Phoenix.Tracker
enables cluster-wide tracking of processes, synchronizing
tracked processes and their metadata across nodes using Phoenix.PubSub
.
Phoenix.Tracker
employs conflict-free replication of state, avoiding the need
for global state or consensus protocols. Instead, each node maintains its own
eventually consistent copy of the tracked list.
Building upon Tracker’s capabilities, Phoenix.Presence
specializes this
functionality for tracking clients connected to Channels - or more specifically
the channel processes they’re connected to. This powers the classic chat app
example, where you want to know the names of other users online in the chat
room.
Phoenix.Presence
additionally integrates with Phoenix.Channel
s for communcation
. It extends the state synchronization of the list of tracked presences
to external channel clients, so they can maintain their own eventually consistent
copy too.
UseCase #2: Phoenix.LiveView
Phoenix.LiveView
is a comparatively younger feature of Phoenix, which also
makes use of Phoenix.Channel
s. It uses the communication between the elixir
node and an external browser over Channels to enable server driven interactivity
on websites work.
Given the primitive of Phoenix.Channel
s already existing LiveView was able
to focus on what data it needs to share with the external client, already having
both websocket and long polling support provided as a means of being able to
talk to a liveview client.
LiveView is kinda interesting in that it doesn’t use the Channel to broadcast messages across the cluster using all the PubSub handling explained in this blog post. It uses Channels just for the communication with a specific and single client.
That is however not to say that applications build using LiveView won’t make use of PubSub or Channels within LiveView.
Our exploration of Phoenix’s real-time capabilities has taken us on a journey through layers of powerful abstractions. Abstractions, which build a powerful, scalable real-time communication system. Each layer builds upon the strengths of those beneath it, providing developers with increasingly high-level tools while maintaining the performance and distributed capabilities of the underlying components.
By understanding these layers, developers can make informed decisions about which abstractions to use for their specific needs, whether it’s low-level process management with Registry or building interactive, multi-user experiences with Channels and Presence.