Pub-Sub Messaging Patterns

Written by Backbone Tutorials Team

Last updated: June 2026 · 9 min read

A WebSocket connection carries everything the server decides to send. That works fine for a single stream of updates, but as an application grows it accumulates many distinct event types that go to different parts of the client. A user receives document patches, presence updates, notification counts, and system messages, and the code that handles each of those should not need to know about the others. Publish-subscribe is the pattern that solves this: producers publish to named topics, consumers subscribe to the topics they care about, and the two sides never hold direct references to each other.

This guide sits inside the Interactive Web cluster and connects the transport layer, WebSocket and SSE, to the application-level message routing that every realtime feature depends on.

Pub-sub fan-out A publisher sends to a topic channel; the broker fans the message out to all subscribers of that channel. Publisher Broker topic: doc:abc:patches Subscriber A Subscriber B Subscriber C publish fan-out
A publisher sends one message to a topic; the broker fans it out to every subscriber of that channel.

The pub-sub model

Pub-sub is one of the most durable patterns in distributed systems because it achieves something deceptively simple: it lets code communicate without either side knowing the other exists.

Producers, topics, and consumers

A publisher emits a message to a named topic and then forgets it. The broker, which may be a server process, an in-memory event bus, or a dedicated message queue, holds the topic and delivers the message to every subscriber that has registered interest. The publisher does not know how many subscribers exist; a subscriber does not know which publisher sent the message. This decoupling makes it straightforward to add a new subscriber without touching the publisher, or to replace a publisher without touching any consumer, which is why the pattern scales well as a codebase grows.

Topic channels and fan-out

On a WebSocket server, a topic channel is usually a string key that maps to a set of connections. Publishing to that channel means iterating over the set and writing to each connection.

Server-side channel registry

// Server: channel registry and fan-out
var channels = new Map(); // topic -> Set of connections

function subscribe(connection, topic) {
  if (!channels.has(topic)) channels.set(topic, new Set());
  channels.get(topic).add(connection);
}

function publish(topic, message) {
  var subscribers = channels.get(topic);
  if (!subscribers) return;
  var payload = JSON.stringify(message);
  subscribers.forEach(function (conn) {
    if (conn.readyState === 1) conn.send(payload);
  });
}

function unsubscribe(connection, topic) {
  var subs = channels.get(topic);
  if (subs) subs.delete(connection);
  if (subs && subs.size === 0) channels.delete(topic);
}

Fan-out cost is proportional to subscriber count per channel. A channel with a thousand subscribers requires a thousand send calls. For large channels this becomes a bottleneck on a single Node.js process; horizontal scaling requires a shared message bus such as Redis Pub-Sub so that a publish on one server process reaches subscribers connected to other processes.

Hierarchical channel namespacing

Flat channel names work for small apps. As channels multiply, a hierarchical naming convention keeps the address space organised and enables prefix routing.

Slash-separated channel paths

// Channel naming convention
// org:{orgId}                       -> org-wide events
// org:{orgId}:doc:{docId}           -> document-level events
// org:{orgId}:doc:{docId}:patches   -> document content changes
// org:{orgId}:doc:{docId}:presence  -> cursor and awareness

subscribe(conn, 'org:acme:doc:abc123:patches');
subscribe(conn, 'org:acme:doc:abc123:presence');

// Wildcard: subscribe to all channels under a document
// (server resolves prefix to all matching channels)
subscribePrefix(conn, 'org:acme:doc:abc123');

A colon-separated hierarchy mirrors the resource model of the application. Prefix subscriptions let a client join an entire subtree, useful when a component mounts and needs all events related to one entity. The server resolves the prefix against the channel registry at subscribe time and subscribes the connection to each matching channel. When the component unmounts, a single unsubscribePrefix call removes all of them.

Subscription management

On a long-running page, subscriptions accumulate and must be cleaned up to avoid memory leaks and phantom deliveries.

Tracking and releasing subscriptions

// Client: subscribe, track, and clean up
var activeTopics = new Set();

function clientSubscribe(ws, topic) {
  ws.send(JSON.stringify({ type: 'subscribe', topic: topic }));
  activeTopics.add(topic);
}

function clientUnsubscribe(ws, topic) {
  ws.send(JSON.stringify({ type: 'unsubscribe', topic: topic }));
  activeTopics.delete(topic);
}

// Unsubscribe all when a component unmounts
function teardown(ws) {
  activeTopics.forEach(function (topic) {
    clientUnsubscribe(ws, topic);
  });
}

Every subscription is a server-side resource. An application that opens channels on every navigation and never closes them leaks memory on the server and continues receiving messages for pages the user has already left. The pattern is to track every subscription in a set and call teardown when the component or view that opened the subscriptions is destroyed. In Backbone, the remove method is the natural place for this cleanup.

Client-side event routing

Messages arrive over a single WebSocket connection. On the client, a dispatcher routes each message to the handler registered for its topic.

A typed message dispatcher

// Client dispatcher: route by topic + message type
var handlers = new Map(); // 'topic:eventType' -> [fn, ...]

function on(topic, eventType, handler) {
  var key = topic + ':' + eventType;
  if (!handlers.has(key)) handlers.set(key, []);
  handlers.get(key).push(handler);
}

function dispatch(msg) {
  var key = msg.topic + ':' + msg.type;
  var fns = handlers.get(key) || [];
  fns.forEach(function (fn) { fn(msg.payload); });
}

ws.addEventListener('message', function (event) {
  dispatch(JSON.parse(event.data));
});

// Usage
on('org:acme:doc:abc123:patches', 'delta', function (payload) {
  editor.applyDelta(payload);
});

Routing by a compound key of topic plus event type keeps handlers specific without nesting conditionals. Adding a new handler for a new event type requires only a new on call; no existing code changes. This is the same principle as the event-driven UI pattern, applied to network messages instead of DOM events.

Pub-sub with Backbone events

Backbone ships a lightweight pub-sub mechanism in its events module. It works for both network messages and in-process component communication.

A shared event broker

// A plain object mixed with Backbone.Events becomes a global bus
var broker = Object.assign({}, Backbone.Events);

// Publisher: fire an event on the broker
function publishLocal(topic, payload) {
  broker.trigger(topic, payload);
}

// Subscriber: any view or model can listen
var DocView = Backbone.View.extend({
  initialize: function () {
    this.listenTo(broker, 'doc:abc123:patch', this.onPatch);
  },
  onPatch: function (payload) {
    this.model.set(payload);
  }
});

Using listenTo rather than on binds the subscription to the view's lifecycle: when the view is removed, Backbone automatically calls stopListening, which removes the handler from the broker and prevents the memory leak described above. The broker pattern bridges the network dispatcher, which fires events on the broker, and the component layer, which subscribes through listenTo, without either knowing about the other. That separation is the pub-sub promise fulfilled at the component level.

Frequently Asked Questions

What is the publish-subscribe pattern?

Publish-subscribe decouples message producers from consumers. A publisher sends a message to a named topic without knowing who will receive it. Subscribers register interest in topics and receive messages when they are published. Neither side holds a direct reference to the other.

What is fan-out in a pub-sub system?

Fan-out is the delivery of one published message to all subscribers of a topic. When a server receives a message on a channel, it iterates over every connection subscribed to that channel and sends the message to each one. The cost scales with the number of subscribers per channel.

How do hierarchical channel names work?

Hierarchical channel names use a separator like a colon or slash to organise topics into a tree. A channel named doc:abc:cursors is a child of doc:abc. This lets servers route messages efficiently and lets clients subscribe to a subtree, receiving all messages under a prefix.

Does Backbone have a built-in pub-sub mechanism?

Backbone events provide a lightweight pub-sub mechanism through listenTo, trigger, and on. For cross-component messaging, a shared event broker object mixed with Backbone.Events acts as a global pub-sub bus: components publish by calling broker.trigger and subscribe by calling listenTo.

Read next: the Interactive Web hub, or continue to presence and awareness to see pub-sub applied to ephemeral state.

See pub-sub applied in a real Backbone app.

The Backbone guide shows how events and collections wire up to live data sources.

Explore the Backbone Guide →