Event-Driven

Publish-Subscribe Pattern

One publisher, a topic, many subscribers — fan-out without coupling

A publisher emits a message to a named topic. The broker delivers it to every subscriber registered on that topic. Neither side knows the other. The same primitive powers Kafka, Redis Pub/Sub, MQTT, and the Observer pattern.

  • PrimitivePublisher → topic → subscribers
  • DeliveryBroadcast (every subscriber)
  • Kafka throughput1M+ msg/sec/broker
  • Redis Pub/Sub~1M msg/sec/server, in-memory
  • OOP equivalentObserver pattern (GoF 1994)
  • Used inKafka, Redis, MQTT, GCP Pub/Sub, SNS

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

Why pub/sub is everywhere

Three parts: a publisher, a topic, and one or more subscribers. The publisher writes a message into a topic. The broker keeps a list of subscribers for that topic and delivers the message to all of them. Done.

What you get from these three primitives is enormous. Decoupling: publisher and subscribers never address each other. Fan-out: N subscribers each receive every message. Independence: subscribers come and go without disturbing the publisher. Substitution: replace a subscriber, the publisher doesn't notice. Replay (in log-based brokers): subscribers can rewind.

Pub/sub is the messaging primitive at the heart of every event-driven system. The Observer pattern is its in-process cousin: same shape — one Subject, many Observers, broadcast on state change — minus the network and the broker.

Pub/sub vs message queue

The single most important distinction in messaging:

  • Queue — each message goes to one consumer. Multiple consumers compete; the broker load-balances across them. Used for work distribution (10 worker processes draining a queue of jobs).
  • Topic (pub/sub) — each message goes to every subscriber. Used for broadcast (one event triggering many independent reactions).

Kafka unifies both with consumer groups. Within a group, partitions are split among consumers (queue semantics). Multiple groups on the same topic each get the full feed (pub/sub semantics). RabbitMQ separates them explicitly: queues vs exchanges. Redis has both PUBLISH/SUBSCRIBE (pub/sub) and BLPOP-style queues.

A worked example: notification fan-out

Your app's notification service publishes a NotificationRequested event to topic notifications. Four subscribers register:

  • EmailSender: renders an email and sends via SES.
  • SMSSender: calls Twilio API.
  • PushSender: sends to APNs / FCM.
  • InAppNotifier: writes a row to the in-app feed.

One publish call — broker.publish("notifications", payload) — and four downstream services react independently. Add Slack later? Subscribe SlackSender to the topic; nothing else changes. Drop SMS? Unsubscribe SMSSender; nothing else changes. The notification service is in blissful ignorance of how many subscribers exist.

Pub/sub in Kafka

Kafka is the most-deployed pub/sub system in production. Five concepts to know:

  1. Topic. A named log: orders.OrderPlaced. Append-only, durable, replicated.
  2. Partition. Topics are split into N partitions for parallelism. Messages with the same key always go to the same partition (preserves order per key).
  3. Producer. Appends messages to a topic. Picks the partition (by key or round-robin).
  4. Consumer. Reads messages from one or more partitions, tracks its own offset.
  5. Consumer group. A set of consumers cooperating to consume a topic; each partition is assigned to one consumer in the group. Two consumer groups on the same topic each get the full message stream — that's the pub/sub fan-out.

Throughput: 1M+ msg/sec/broker on commodity hardware. Latency: 5-10 ms p99 publish, 1-5 ms tail read. Retention: configurable (default 7 days; many shops set to forever for event sourcing).

from confluent_kafka import Producer, Consumer

# Publisher
producer = Producer({"bootstrap.servers": "kafka:9092"})
producer.produce(
    topic="notifications",
    key=user_id,
    value=json.dumps({"type": "order_placed", ...}),
)
producer.flush()

# Subscriber (one of many)
consumer = Consumer({
    "bootstrap.servers": "kafka:9092",
    "group.id": "email-sender",        # group = isolation per subscriber type
    "auto.offset.reset": "earliest",
})
consumer.subscribe(["notifications"])
while True:
    msg = consumer.poll(1.0)
    if msg and not msg.error():
        send_email(json.loads(msg.value()))
        consumer.commit(msg)

Redis Pub/Sub

Redis offers a much simpler pub/sub: in-memory, no persistence, no replay. Latency: sub-millisecond. Throughput: ~1M msg/sec per server. Limitation: subscribers must be connected at publish time — a subscriber that connects after a publish never sees the missed messages.

import redis
r = redis.Redis()

# Publisher
r.publish("notifications", json.dumps({"type": "order_placed"}))

# Subscriber
pubsub = r.pubsub()
pubsub.subscribe("notifications")
for msg in pubsub.listen():
    if msg["type"] == "message":
        handle(json.loads(msg["data"]))

Use Redis Pub/Sub for low-latency in-process broadcast (live dashboards, chat-room presence). Use Kafka for durable, replayable event streams. Redis Streams (newer) sits between — persistent and consumer-group-aware.

Pub/sub brokers compared

KafkaRedis Pub/SubRabbitMQNATSGCP Pub/SubMQTT
DurabilityDisk + replicationNone (in-memory)Disk-optionalOptional (JetStream)Managed durableOptional (QoS 1/2)
Throughput1M+ msg/sec/broker~1M msg/sec/server~50k msg/sec/node1M+ msg/secAutoscale~10k msg/sec/broker
p99 latency5-10 ms<1 ms1-10 ms<10 ms~50 ms10-100 ms
Replay historyYes (offsets)NoNo (consumed)JetStream: yesYes (snapshots)Retained: yes
Subscription modelConsumer groupsChannel SUBSCRIBEQueue bindingsSubject subscriptionSubscription resourceTopic + wildcard
WildcardsRegex consumersPSUBSCRIBETopic exchangeSubject wildcardsSubscription filter+ and # wildcards
Typical useStream + ESReal-time fan-outWork queuesIn-cluster messagingManaged event ingestIoT / edge

The Observer pattern connection

Pub/sub's in-process ancestor is the Observer pattern (GoF 1994). Same shape, different scope:

class Subject:
    def __init__(self):
        self._observers = []
    def subscribe(self, observer):
        self._observers.append(observer)
    def publish(self, event):
        for observer in self._observers:
            observer.notify(event)

class EmailObserver:
    def notify(self, event):
        send_email(event)

class AnalyticsObserver:
    def notify(self, event):
        record_metric(event)

subject = Subject()
subject.subscribe(EmailObserver())
subject.subscribe(AnalyticsObserver())
subject.publish({"type": "order_placed"})

This is exactly pub/sub minus the network, the durability, and the cross-process scope. Every UI framework's event model (DOM events, React state subscriptions, RxJS) is Observer-shaped. Every distributed messaging system is pub/sub-shaped. The pattern scales from "function call list" to "trillion messages per day" by changing the transport, not the model.

Ordering, delivery, and exactly-once

  • Per-key ordering. Kafka guarantees order within a partition. Use the entity ID (order_id, user_id) as the partition key to preserve order for that entity.
  • At-least-once by default. Subscribers may see duplicates due to network retries. Idempotency keys at the consumer are mandatory.
  • Kafka EOS (2017). Transactional producer + transactional consumer + transactional sink — exactly-once within Kafka. Doesn't extend to external sinks unless the sink also participates.
  • Effectively-once. The pragmatic target: at-least-once delivery + idempotent consumers = no duplicate observable effect.
  • Backpressure. Fast publisher + slow consumer = lag. Kafka shows this as consumer-group lag metrics; alert when lag exceeds threshold.

Real-world pub/sub at scale

  • LinkedIn Kafka. Invented Kafka. ~7 trillion msg/day across activity events, feed updates, ML feature streams.
  • Slack. ~3.5B real-time events/day flow through Kafka and Redis Pub/Sub for the WebSocket fan-out.
  • Twitter. Original timeline fan-out was Redis Pub/Sub; later moved to a hybrid push/pull architecture for scale.
  • AWS SNS. Cloud pub/sub at scale; ~1 trillion msg/month across SNS topics globally.
  • MQTT broker fleets. Tesla, smart-home platforms (Home Assistant, AWS IoT) use MQTT pub/sub for device-to-cloud at millions of devices per region.
  • Google Cloud Pub/Sub. Manages every YouTube view event for analytics — hundreds of billions/day.

Common misconceptions

  • "Pub/sub equals broadcast equals fan-out." Yes — and that means every subscriber pays the cost. If you only want one consumer to process, use a queue.
  • "Kafka and Redis Pub/Sub are interchangeable." No. Kafka persists; Redis Pub/Sub doesn't. Choose based on durability requirements.
  • "Subscribers always see every message." Only if they're connected (Redis) or within retention (Kafka). Missed messages outside the window are lost.
  • "Ordering is global." No — per-partition only (in Kafka). Use partition keys to scope ordering to an entity.
  • "Subscribers process in real time." They process when they consume. Lag is normal. Monitor it.
  • "Pub/sub gives you exactly-once." At-least-once is the default. Combine with idempotent consumers for effectively-once.
  • "Adding a subscriber is free." Each subscriber processes every message — that's CPU and IO. Free for the publisher; not free for the broker or subscriber.

Performance characteristics

  • Publish latency: 1-50 ms typical depending on broker. Redis fastest, Kafka durable, MQTT highest for QoS 2.
  • Fan-out cost: O(N) per message at the broker — N subscribers means N delivery operations. Brokers like Kafka push this to consumers (pull model), Redis pushes from broker (push model).
  • Subscriber state: Kafka tracks offsets, Redis Pub/Sub tracks active connections, RabbitMQ tracks queue bindings. State proportional to (topics × subscribers).
  • Storage (Kafka): ~500 bytes/event compressed; 250 GB / 1B events. Retention configurable.
  • Replay cost: rewinding a Kafka offset to 0 replays the whole topic — useful for new consumers backfilling history.

Frequently asked questions

What's the difference between pub/sub and a message queue?

A queue delivers each message to ONE consumer (load-balanced across competing consumers). Pub/sub delivers each message to EVERY subscriber on the topic (broadcast / fan-out). If you have three consumers on a queue and publish one message, exactly one of them sees it — typical load balancing. If you have three subscribers on a topic and publish one message, all three see it. Kafka's consumer-group model unifies both: each consumer group is a queue (one consumer per partition), and multiple consumer groups on the same topic give you pub/sub fan-out. RabbitMQ separates them explicitly with queues and exchanges.

How does Kafka implement pub/sub?

Kafka's primitive is a partitioned, durable log called a topic. Producers append messages to a topic; messages are placed into one of N partitions based on a key (or round-robin). Consumers belong to a 'consumer group' — within a group, partitions are divided across consumers. Multiple consumer groups on the same topic each get the full message stream — that's the pub/sub fan-out. Performance: 1M+ messages/sec/broker, 5-10 ms p99 publish latency, days-to-forever retention. Consumer offsets are tracked per (group, topic, partition), so a consumer can replay history by resetting its offset.

Is pub/sub the same as the Observer pattern?

Observer is the in-process OOP version: an object (Subject) maintains a list of dependents (Observers) and notifies them on state change. Pub/sub is the distributed version with the same shape but a broker in the middle. Both decouple producers from consumers. Both broadcast. The differences are scale and durability: Observer runs in one process, lives in memory, can't survive a restart; pub/sub runs across machines, persists messages, and gives you replay. GoF (1994) is the canonical Observer reference; Pub/Sub the distributed term was popularized by TIBCO and IBM MQ in the 1990s for messaging middleware.

When does pub/sub break down?

Three failure modes: (1) Subscribers that need synchronous responses — pub/sub is fire-and-forget; if you need 'did the subscriber succeed?' you need request-reply over the broker, which loses most of the decoupling. (2) Strict ordering across all subscribers — Kafka guarantees order within a partition, not across them; if 'order_paid' must arrive before 'order_shipped' globally, partition both events on the same key. (3) Backpressure on slow subscribers — a topic with 100k msg/sec and one subscriber doing 1k/sec accumulates lag. Solutions: scale subscriber horizontally (more partitions, more consumer instances), batch processing, or drop on overload.

How are subscribers managed?

Three styles: (1) Static subscription — subscriber is configured at deploy time (Kafka consumer subscribed to topic 'orders'). Simple, common. (2) Dynamic subscription — subscriber registers/unregisters at runtime (Redis Pub/Sub SUBSCRIBE command). Good for chat, real-time feeds, ephemeral interest. (3) Wildcard / pattern subscription — subscribe to all topics matching 'orders.*' (Kafka regex consumers, MQTT wildcards). Useful for cross-cutting concerns like audit logging. Brokers maintain subscriber state internally; in Kafka via the __consumer_offsets topic and consumer-group coordinator.

What about message ordering and exactly-once semantics?

Ordering: Kafka preserves order within a single partition; messages with the same key always go to the same partition. Order across partitions is undefined. To preserve order for an entity, use that entity's ID as the partition key. Exactly-once: pub/sub brokers offer at-least-once delivery natively. Kafka added transactional / exactly-once-semantics (EOS) in 2017 — combines idempotent producer + transactional consumer + transactional sink. EOS works within Kafka but breaks at the boundary (e.g., consuming from Kafka and writing to Postgres needs application-level idempotency). For practical exactly-once, pair at-least-once delivery with idempotent consumers that dedupe by event ID.