Pub/sub is a messaging pattern where publishers send messages to topics on a broker, and every subscriber to a topic gets a copy. Its whole value is what the parties don’t know: publishers never learn who is listening, subscribers never learn who sent — the broker’s indirection decouples them in space (different systems), time (subscribers may be offline), and synchronization (publish returns immediately). Add subscribers without touching publishers; that is the pattern’s quiet superpower.
Key takeaways
| Question | Answer |
|---|---|
| The cast | Publisher → message → topic → broker → subscribers (each gets a copy) |
| vs. a queue | A queue distributes work to one consumer; pub/sub duplicates it to all |
| vs. observer | Observer is in-process and synchronous; pub/sub is brokered and async |
| The default stance | At-least-once delivery + idempotent handlers |
| The fine print | No replies, no default ordering, no atomic transactions across the broker |
The pattern in code
Every pub/sub API reduces to two verbs, whatever the broker:
// Subscribers — register interest in a topic
broker.subscribe('orders.paid', (msg) => fulfill(msg)); // service A
broker.subscribe('orders.paid', (msg) => notifyUser(msg)); // service B — its own copy
// Publisher — fire and forget
broker.publish('orders.paid', { orderId: 'o-1187' });
// The publisher never learns who received it — or whether anyone did.
The same shape at the client edge, where a live-query subscription is the topic and a database write is the publish:
// JavaScript / Node.js — Back4app JS SDK
// Pub/sub at the edge: live queries subscribe, saves publish
// Subscriber — register interest in a topic
const topic = new Parse.Query('Event');
topic.equalTo('topic', 'orders');
const sub = await topic.subscribe();
sub.on('create', (event) => handle(event.get('payload')));
// Publisher — fire and forget; no knowledge of subscribers
const event = new Parse.Object('Event');
event.set('topic', 'orders');
event.set('payload', { orderId: 'o-1187', status: 'paid' });
await event.save(); // Flutter / Dart — Back4app Flutter SDK
// Pub/sub at the edge: live queries subscribe, saves publish
// Subscriber — register interest in a topic
final liveQuery = LiveQuery();
final topic = QueryBuilder<ParseObject>(ParseObject('Event'))
..whereEqualTo('topic', 'orders');
final sub = await liveQuery.client.subscribe(topic);
sub.on(LiveQueryEvent.create, (event) => handle(event.get('payload')));
// Publisher — fire and forget; no knowledge of subscribers
final event = ParseObject('Event')
..set('topic', 'orders')
..set('payload', {'orderId': 'o-1187', 'status': 'paid'});
await event.save(); // iOS / Swift — Back4app Swift SDK
// Pub/sub at the edge: live queries subscribe, saves publish
// Subscriber — register interest in a topic
let topic = Event.query("topic" == "orders")
let sub = try await topic.subscribe()
sub.handleEvent { _, e in
if case .created(let event) = e { handle(event.payload) }
}
// Publisher — fire and forget; no knowledge of subscribers
var event = Event()
event.topic = "orders"
event.payload = ["orderId": "o-1187", "status": "paid"]
try await event.save() // Android / Kotlin — Back4app Android SDK
// Pub/sub at the edge: live queries subscribe, saves publish
// Subscriber — register interest in a topic
val client = ParseLiveQueryClient.Factory.getClient()
val topic = ParseQuery.getQuery<ParseObject>("Event")
topic.whereEqualTo("topic", "orders")
val sub = client.subscribe(topic)
sub.handleEvent(SubscriptionHandling.Event.CREATE) { _, event ->
handle(event.getJSONObject("payload"))
}
// Publisher — fire and forget; no knowledge of subscribers
val event = ParseObject("Event")
event.put("topic", "orders")
event.put("payload", JSONObject(mapOf("orderId" to "o-1187", "status" to "paid")))
event.save() How pub/sub works
The broker does four jobs: accept publishes and return immediately; match each message against subscriptions (by topic name, hierarchically — orders.* — or by content attributes); deliver an independent copy per subscriber, retrying per its guarantee level; and retain messages for offline subscribers where durability is configured — the time-decoupling that lets a service deploy at noon and catch up on what it missed. One vocabulary note worth its sentence: a message is the envelope; an event — “this happened, past tense” — is what pub/sub usually carries, and whether it carries just the fact or the full state is Fowler’s event-notification vs. event-carried-state-transfer distinction.
Pub/sub vs. message queues vs. observer pattern
The two confusions, settled in one table:
| Pub/sub | Message queue | Observer | |
|---|---|---|---|
| Delivery | Every subscriber gets a copy | Exactly one worker consumes each message | Subject calls each observer |
| Purpose | Broadcast events | Distribute work | React to state in-process |
| Coupling | None — broker-mediated | None — queue-mediated | Direct object references |
| Timing | Async | Async | Usually synchronous |
| Boundary | Across processes/systems | Across workers | Inside one process |
| Canonical shape | 1 event → N reactions | N jobs → M workers | 1 object → its listeners |
The soundbite that survives the meeting: a queue distributes work; pub/sub duplicates it. And observer is not “small pub/sub” — the broker’s indirection is precisely what observer lacks, which is why refactoring an observer into pub/sub is an architectural change, not a rename. In practice brokers offer both shapes: consumer groups turn a topic into a queue per group (Kafka’s signature move), fanout exchanges turn a queue system into pub/sub.
Delivery guarantees: at-most-once vs. at-least-once vs. exactly-once
| Guarantee | Failure mode | Cost | Your handler must | Choose when |
|---|---|---|---|---|
| At-most-once | Messages can vanish | Cheapest, fastest | Tolerate gaps | Ephemeral data: presence, tickers, metrics |
| At-least-once | Duplicates arrive | Acks + retries | Be idempotent | The default for business events |
| Exactly-once | Constrained, complex | Dedup + coordination | Still be idempotent | Narrow paths where brokers support it |
MQTT’s QoS levels 0/1/2 map to these three exactly — a tidy confirmation the trichotomy is fundamental, not vendor marketing. The engineering truth: end-to-end exactly-once is unachievable in general (the Two Generals’ problem wearing a broker badge); systems that advertise it combine at-least-once delivery with deduplication. Which is why the pattern’s real contract is written in your handler: at-least-once + idempotency — dedup on a message ID, use upserts keyed on the entity, or version-check before applying — turns duplicates from bugs into no-ops.
Ordering, dead letters, and topic design
Three operational realities the intro diagrams skip. Ordering: fan-out to parallel consumers has no global order; brokers restore it per partition or ordering key at the price of parallelism — design handlers to apply events by entity ID and version, not arrival sequence, and the problem mostly dissolves. Poison messages: a message whose processing always throws will retry forever under at-least-once; cap delivery attempts and route failures to a dead-letter queue for triage, or one bad event stalls the topic. Topic design: name hierarchically (orders.paid, orders.refunded, wildcard orders.*), keep granularity at the level subscribers actually differ on, and evolve message schemas additively — subscribers ignore unknown fields; breaking changes get a new versioned topic (orders.v2) with a migration window, exactly like an API version.
Common use cases
- Microservice integration — services owned by different teams reacting to each other’s events without direct calls or shared deploy schedules.
- Real-time client features — chat fan-out, live dashboards, presence: pub/sub over WebSockets at the edge.
- IoT telemetry — thousands of devices publishing over MQTT; consumers from alerting to analytics subscribing independently.
- Cache invalidation and replication — one write published, every cache and replica notified.
- Parallel processing pipelines — one event triggering fulfillment, notification, and analytics simultaneously, each at its own pace.
Should you use pub/sub? A decision matrix
| Pub/sub fits when… | Prefer something else when… |
|---|---|
| One event interests several independent consumers | One consumer: a queue, or just a call |
| Producers and consumers deploy independently | Caller needs the answer now → request-response |
| Eventual consistency is acceptable | Atomicity across parties required → transactions/Saga |
| Subscribers come and go over time | Strict global ordering is non-negotiable |
| Spiky load needs buffering between systems | The whole system fits in one process → observer |
Limitations and trade-offs
- Debugging crosses a broker. “Who consumed this and why did it fail?” needs correlation IDs and tracing; the decoupling that frees teams also hides causality.
- No replies by design. Workflows needing responses must model them as further messages — added latency and state machines where a function call once sufficed.
- The broker is infrastructure. Availability, capacity, security, and retention policies of the messaging layer become platform work; a down broker is an outage multiplier.
- Publisher blindness cuts both ways. Publishers can’t observe subscriber health; a silently failing consumer loses data unless dead-letter monitoring exists.
- Guarantees cost throughput. Ordering keys serialize, exactly-once coordinates, durability persists — each strengthening of semantics spends performance; buy only what the data needs.
Pub/sub on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. The pattern shows up here at the client edge: Live Queries are pub/sub where the query is the topic — subscribers register a predicate, any write that matches is published to every subscriber over a managed WebSocket fleet, with permissions checked per subscriber and fan-out handled by the platform, as in the code tabs above. Server-side reactions compose the same way: Cloud Code triggers subscribe to data changes (afterSave as a topic subscription in spirit), and scheduled functions drain work published as rows. For heavy inter-service streaming you would still reach for a dedicated broker like Kafka or RabbitMQ — but for the common product cases, broadcast-on-change ships with the backend.
Frequently asked questions
What is pub/sub in simple terms?
A messaging pattern where senders (publishers) post messages to named topics on a broker, and every component that subscribed to that topic receives its own copy — publishers and subscribers never know about each other. Think radio broadcast: the station transmits on a frequency; whoever tunes in, hears it.
What is the difference between pub/sub and a message queue?
A queue distributes work; pub/sub duplicates it. In a queue, each message is consumed by exactly one worker — the tool for spreading jobs across a pool. In pub/sub, every subscriber to the topic gets a copy — the tool for broadcasting events to independently interested parties.
What is the difference between pub/sub and the observer pattern?
Observer is in-process: the subject holds direct references to its observers and usually calls them synchronously. Pub/sub inserts a broker between the parties, making them fully decoupled, typically asynchronous, and able to live in different processes, languages, and deployments. One is a design pattern inside an app; the other is an architecture between systems.
How is pub/sub different from request-response?
Request-response is synchronous and one-to-one — the caller blocks until an answer returns. Pub/sub is asynchronous, one-way, and one-to-many: publish returns immediately and no reply exists. If a subscriber must answer, that is a separate message on a reply channel, not a return value.
What is a topic in pub/sub?
A named logical channel that categorizes messages: publishers address the topic, and the broker delivers each message to all of that topic's subscribers. Filtering can be topic-based (subscribe by name, often hierarchically — orders.*) or content-based, where the broker matches on message attributes.
What delivery guarantees does pub/sub offer?
Three levels. At-most-once: fire and forget — fast, may lose messages. At-least-once: retry until acknowledged — the common default, produces occasional duplicates. Exactly-once: deduplication plus coordination — expensive, constrained, and impossible to guarantee end-to-end in general. The practical stance: at-least-once delivery with idempotent handlers.
Does pub/sub guarantee message ordering?
Not by default — messages fan out to parallel subscribers, and global order fights parallelism. Brokers can order within a partition or ordering key at a throughput cost. The robust design assumes ordering only where explicitly configured and writes handlers that tolerate out-of-order arrival.
Is Kafka pub/sub?
Yes, with a twist: Apache Kafka implements publish-subscribe semantics over a partitioned, durable, replayable commit log. Within a consumer group it behaves like a queue (each message to one member); across groups it behaves like pub/sub (each group gets a copy) — which is why it is usually called event streaming.
When should you not use pub/sub?
When the caller needs an immediate answer (request-response), when operations must be atomic across producer and consumers (a broker cannot join a transaction — see the Saga pattern), when strict global ordering is required, or when the system is small enough that a broker adds more operational surface than it removes.