|claim|login
RepoCritics — Review. Share. Archive. Every open-source repo.

phoenixframework/phoenix_pubsub

Wiki: phoenixframework/phoenix_pubsub

Source: https://github.com/phoenixframework/phoenix_pubsub

Last synced 2026-07-18 · 1377 words · Edit wiki on GitHub →

phoenixframework/phoenix_pubsub

> Distributed pub/sub and presence tracking for the BEAM — the messaging layer under every Phoenix Channel and LiveView.

GitHub repo · Official website · License: MIT

Overview

Phoenix.PubSub is the topic-based publish/subscribe library extracted from the Phoenix web framework in late 20151. It does two things: fan out messages to subscriber processes across an Erlang cluster (Phoenix.PubSub), and track which processes are present on which topics with an eventually-consistent CRDT (Phoenix.Tracker, the engine behind Phoenix.Presence). Every Phoenix application depends on it transitively — Channels, LiveView updates, and Presence all ride on it — which explains the mismatch between its modest 764 GitHub stars and its actual footprint: it is infrastructure people use daily without ever visiting the repo.

The defining tradeoff is that distribution comes "for free" only if you accept the BEAM's terms. PubSub piggybacks on Distributed Erlang: if your nodes are clustered, broadcasts reach every node with zero extra infrastructure — no Redis, no broker, no sidecar. If your nodes cannot form an Erlang cluster (some PaaS environments, cross-VPC setups), the default adapter silently degrades to single-node behavior and you must bolt on the separate Redis adapter. Delivery is fire-and-forget: at-most-once, unordered across senders, no persistence, no replay, no backpressure. It is a signaling fabric, not a message queue — a distinction that trips up teams who reach for it as one.

Maintenance is mature-and-quiet rather than active-and-churning: last push May 2026, 14 open issues, and a 2.x API that has been stable since 2020. For a foundational library, that is a feature.

Getting Started

Add the dependency in mix.exs (already present in any Phoenix app):

def deps do
  [{:phoenix_pubsub, "~> 2.0"}]
end

Start an instance under your supervision tree, then subscribe and broadcast:

# application.ex
children = [
  {Phoenix.PubSub, name: MyApp.PubSub}
]

# any process (a LiveView, a GenServer, an IEx shell)
Phoenix.PubSub.subscribe(MyApp.PubSub, "user:123")
Phoenix.PubSub.broadcast(MyApp.PubSub, "user:123", {:user_updated, %{id: 123}})

# the subscriber receives it as a plain message
receive do
  {:user_updated, user} -> IO.inspect(user)
end

Messages arrive in the subscriber's mailbox like any Erlang message — in a LiveView you handle them in handle_info/2. local_broadcast/3 skips remote nodes when you only need node-local fan-out.

Architecture / How It Works

The library is two loosely coupled systems sharing a transport.

PubSub core. Since v2.0, local subscriptions live in a sharded Elixir Registry with duplicate keys2: subscribe/2 registers the caller's pid under the topic, and a local broadcast walks the registry partitions and send/2s each subscriber directly. There is no broker process on the hot path — the v1 design's per-node GenServer bottleneck was removed in the 2.0 rewrite that shipped alongside Phoenix 1.5 in April 20203. Remote delivery goes through an adapter behaviour (Phoenix.PubSub.Adapter). The default Phoenix.PubSub.PG2 adapter uses Erlang process groups (:pg on OTP 23+, the deprecated :pg2 before that) to locate the PubSub process on each remote node and sends one message per node; the receiving node then does its own local registry dispatch. Fan-out cost is therefore O(nodes) on the wire and O(local subscribers) per node.

Custom dispatch. broadcast/4 accepts a dispatcher module implementing dispatch/3, and subscribers can attach metadata at subscribe time. Phoenix Channels exploit this for "fastlaning": the message is encoded to its wire format once, and the raw frame is handed to each websocket transport process, avoiding N redundant JSON encodings for N subscribers. This hook is available to any application, not just Phoenix.

Tracker (presence). Phoenix.Tracker maintains replicated presence state using an ORSWOT delta-CRDT (observed-remove set without tombstones)4. Each node tracks its local processes and gossips deltas to peers via heartbeats over the PubSub transport itself. State is sharded across a configurable pool. Node failures are handled in two phases: a temporary "down" grace period (state retained), then a permanent-down cutoff after which the dead replica's state is dropped and, on rejoin, fully re-synced. The result is eventual consistency — two nodes can briefly disagree about who is online, by design.

Coupling story: PubSub has no dependency on Phoenix (usable in any Mix project), but it is hard-coupled to BEAM distribution semantics — full-mesh Distributed Erlang, node names, EPMD or an alternative discovery mechanism.

Production Notes

  • Clustering is your problem. PubSub does not form the cluster; it assumes one exists. Production deploys pair it with libcluster or the simpler dns_cluster for node discovery on Kubernetes/Fly.io. Forgetting this yields the classic symptom: everything works on one node, presence and broadcasts mysteriously miss users behind a load balancer with two.
  • No delivery guarantees. Broadcasts to a node that is briefly unreachable are lost, not queued. After a netsplit heals, Tracker state converges (CRDT), but plain PubSub messages sent during the split are gone. Anything requiring durability, ordering, or acknowledgment belongs in a real queue.
  • No backpressure. Broadcast is send/2 under the hood. A slow subscriber accumulates an unbounded mailbox; a hot topic with thousands of local subscribers makes every broadcast pay that fan-out cost synchronously in the caller-side dispatch loop. Mitigations: shard topics ("room:123" not "rooms"), use local_broadcast where cross-node delivery isn't needed, and put an intermediary GenServer in front of expensive subscribers.
  • Full-mesh scaling ceiling. Distributed Erlang meshes every node with every node; practical PubSub clusters run in the tens of nodes, not hundreds. Beyond that you need topology help (Partisan-style overlays or an external broker), which this library does not provide.
  • Tracker churn cost. Presence gossip is cheap at steady state but grows with join/leave churn and cluster size. Very churny topics (large public rooms with fast-cycling anonymous users) can make heartbeat deltas a measurable share of inter-node traffic. Tune shard pool size and consider whether you need presence at all on such topics.
  • 1.x → 2.0 upgrade was breaking: adapter configuration moved out of the Endpoint config into an explicit supervision child, the adapter API changed, and the Redis adapter required a coordinated major bump of phoenix_pubsub_redis3. Anything written against 2.x since 2020 has needed essentially no changes since — the API surface is small and frozen in practice.

When to Use / When Not

Use when:

  • You're on the BEAM and need intra-cluster fan-out (LiveView updates, chat, notifications, cache invalidation signals) without external infrastructure.
  • You need "who is online" presence and can accept eventual consistency — Tracker is one of the few production-hardened CRDT presence implementations anywhere.
  • You already run Phoenix: it is installed, supervised, and battle-tested in your stack whether you noticed or not.

Avoid when:

  • You need durable, ordered, acknowledged, or replayable messaging — use a broker or a log, not pub/sub over process mailboxes.
  • Consumers live outside the BEAM (polyglot services) — PubSub speaks Erlang messages, not a wire protocol.
  • Your nodes cannot form a Distributed Erlang cluster and you don't want to operate the Redis adapter.
  • You expect hundreds of nodes — full-mesh distribution won't carry you there.

Alternatives

  • phoenixframework/phoenix_pubsub_redis — official adapter; use when nodes can't cluster over Distributed Erlang but share a Redis.
  • nats-io/nats-server — use when you need lightweight pub/sub across polyglot services, not just BEAM processes.
  • rabbitmq/rabbitmq-server — use when you need durable queues, acks, and routing rather than fire-and-forget fan-out.
  • redis/redis — use its built-in pub/sub when you already run Redis and need simple cross-language channels.
  • elixir-lang/elixir — the standard-library Registry covers single-node local pub/sub with zero dependencies; PubSub only earns its keep once you cluster.

History

VersionDateNotes
0.x2015-11Extracted from the Phoenix framework into a standalone library1.
1.02016-06Stable release used by Phoenix 1.2; PG2 and Redis adapters, per-node local server design.
2.02020-04Rewrite shipped with Phoenix 1.5: Registry-based sharded local dispatch, new adapter behaviour, explicit supervision, :pg support3.
2.12022Maintenance series — current stable line; small fixes and OTP compatibility, no API changes of note.

References

  1. ^ Repository created 2015-11-10, split out of phoenixframework/phoenix. https://github.com/phoenixframework/phoenix_pubsub
  2. ^ Phoenix.PubSub documentation, HexDocs. https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html
  3. ^ Chris McCord, "Phoenix 1.5.0 released" — 2020-04-22, notes the PubSub 2.0 migration. https://www.phoenixframework.org/blog/phoenix-1-5-final-released
  4. ^ Phoenix.Tracker documentation (ORSWOT CRDT, replica lifecycle), HexDocs. https://hexdocs.pm/phoenix_pubsub/Phoenix.Tracker.html

Tags

elixir, erlang, pubsub, presence, crdt, distributed-systems, real-time, phoenix, messaging, beam