Benjamin Milde

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.

Registry

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.

PhoenixPubSubRegistryRegistry

Including external clients: Phoenix.Channel

With Phoenix.Channels 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.

PhoenixPubSubRegistryRegistryClientClientClient

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.Channels 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.Channels. 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.Channels 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.