Skip to content
Katabench
Try free
8 min read The Katabench team

Message Queue vs Event Bus: Choose the Topology

Compare message queues, event buses, and pub/sub by delivery topology. Choose competing workers or fan-out, then design retries and ordering honestly.

An order is placed. Billing must charge it, inventory must reserve stock, email must send a receipt, and analytics would like to count it. Should the application use a message queue or an event bus?

The question sounds like a product choice, but it hides two architecture decisions:

  1. Is the message a job that one logical handler must perform, or a fact that independent handlers may react to?
  2. Within each handler, should one instance receive the delivery or should every instance receive a copy?

A modern broker can often implement both answers. Azure Service Bus has queues plus topics and subscriptions. NATS has subject fan-out plus queue groups and durable JetStream consumers. Kafka has topics plus consumer groups. Buying an "event bus" does not remove queues, and using a "message queue" product does not forbid publish/subscribe.

Start with the delivery topology, then choose the broker feature that implements it.

Same broker, different delivery

Who is supposed to receive this message?

topology before product

Work queue

one logical handler

GenerateInvoice
Producer
Queue
Worker 1
Worker 2
Worker 3

one worker receives each delivery

Event bus / topic

zero to many logical handlers

OrderPlaced
Publisher
Topic
Billing
Email
Analytics

each subscription receives its own copy

An event subscription can itself use competing workers. Fan-out between services and load balancing within one service are separate choices.

A queue answers "which worker will do this job?" An event bus answers "which independent systems need to know this happened?" One event subscription can still use a queue of competing workers.

The short answer

Use a message queue when one logical capability owns the work. Several instances may compete for throughput, but only one should handle each delivery. Image resizing, invoice generation, report exports, and background email jobs have this shape.

Use an event bus or durable pub/sub topic when the publisher announces a fact and does not own the set of reactions. Each interested capability gets its own subscription and its own copy. An OrderPlaced event might feed billing, inventory, notifications, and analytics independently.

Use both when an event fans out to several services and each service scales horizontally. The bus copies OrderPlaced into a billing subscription and an inventory subscription. Three billing instances then compete for messages inside the billing subscription. Fan-out happens between logical consumers; load balancing happens within one logical consumer.

Microsoft's current Service Bus queue and topic guidance draws the same boundary: a queue distributes a message to one competing consumer, while every topic subscription receives a copy.

Commands belong to an owner; events do not

Message names reveal the intended topology. A command is imperative and has one business owner:

public sealed record GenerateInvoice(
    Guid MessageId,
    Guid OrderId,
    decimal Total);

Someone requested a specific effect. If two independent services both believe they own GenerateInvoice, the architecture has duplicated responsibility. Put the command on the invoice capability's queue and scale that capability with competing workers.

An event is past tense and records a fact:

public sealed record OrderPlacedV1(
    Guid MessageId,
    Guid OrderId,
    Guid CustomerId,
    decimal Total,
    DateTimeOffset OccurredAt);

The ordering service states what happened. It should not need to know that billing, email, or a future fraud model subscribes. Each subscriber owns its reaction and can deploy, retry, pause, or fall behind without changing the publisher.

This is a semantic rule, not a naming trick. Calling SendReceipt an event does not make fan-out correct. Calling OrderPlaced a command does not give the publisher the right to coordinate every future reaction.

Queue and event-bus topology side by side

Decision Work queue Event bus / pub-sub
Message meaning A job or command to perform A fact that already happened
Logical recipients One owner Zero to many independent subscribers
Horizontal scale Workers compete for each delivery Each subscription can have competing workers
Backlog One queue for the capability Separate backlog and progress per subscription
Failure isolation Poison work can block or dead-letter one queue One subscriber can fail without blocking the others
Adding a consumer Changes who owns or shares the work Adds a new independent reaction
Filtering Usually route to a different queue Often filter into subscriptions by event type or metadata

The separate backlog is the operational difference teams underestimate. If analytics is offline for six hours, billing should not wait behind its messages. Independent subscriptions let billing stay current while analytics accumulates and later drains its own backlog.

A queue is better when that independence would be a bug. If three workers all receive the same image resize command, they waste compute and race to write the same output. Competing consumption lets any healthy instance claim the work while keeping one logical effect.

"Event bus" is an overloaded name

Before evaluating products, ask what the team means by event bus. It may refer to:

  • an in-process mediator that dispatches objects inside one application;
  • a best-effort pub/sub channel that only reaches currently connected subscribers;
  • a durable broker with acknowledgements, redelivery, replay, and independent subscriptions;
  • a cloud event router that filters events across accounts and managed services.

Those systems have different failure models. An in-memory mediator does not survive a process crash. Core NATS pub/sub is intentionally lightweight and at-most-once. JetStream adds persistence, acknowledgements, redelivery, and replay. A cloud router may deliver durably but impose different ordering, throughput, filtering, and retention limits.

The official NATS queue-group documentation shows how subscribers with the same queue name load-balance deliveries so only one group member is chosen. Subscribers without that queue group still receive normal fan-out. The same NATS server can therefore express both topologies; JetStream is the persistence choice, not a synonym for either topology.

Write down the guarantees rather than accepting the product category:

orders.placed.v1
  billing subscription   -> one of N billing workers
  inventory subscription -> one of N inventory workers
  email subscription     -> one of N email workers

That statement identifies three independent backlogs and one competing worker pool per backlog. It is more useful than "we use an event bus."

Delivery guarantees do not come from the diagram

A queue icon does not guarantee exactly-once processing. A subscriber can finish its database write and crash before acknowledging the message. The broker cannot know whether the effect happened, so a reliable system redelivers. The consumer must make the second delivery harmless.

Give every message a stable identifier and store it with the consumer's effect in one local transaction:

CREATE TABLE processed_messages (
    consumer_name text NOT NULL,
    message_id uuid NOT NULL,
    processed_at timestamptz NOT NULL,
    PRIMARY KEY (consumer_name, message_id)
);

The key includes consumer_name because billing and inventory must each process the same event once. One subscriber's acknowledgement or inbox entry cannot stand in for another subscriber's work. The idempotency guide covers why at-least-once delivery plus deduplication is the honest model.

Publication has a separate crash window. Saving an order and publishing its event are two writes to different systems. The transactional outbox pattern stores the business change and outgoing message in one database transaction, then lets a dispatcher publish it. Queue versus bus does not close that gap.

Ordering is narrower than most diagrams imply

Neither topology should promise global order casually. Multiple publishers, partitions, retries, redeliveries, and competing consumers can all change observation order. A single FIFO queue may preserve enqueue order yet still complete work out of order when several workers process at different speeds.

Name the boundary that requires ordering. An order's status transitions may need sequence numbers within one OrderId; unrelated orders usually do not need to block one another. Use an aggregate or partition key, include a monotonic sequence in the contract, and make consumers detect gaps or stale updates where the business requires it.

Global ordering often converts an independent system into one slow lane. Pay for it only when the domain can explain the invariant it protects.

Choose with five questions

Ask these before comparing RabbitMQ, NATS, Kafka, Azure Service Bus, SQS, SNS, or EventBridge:

  1. Is this intent or fact? A command asks one owner to act; an event lets independent owners react.
  2. How many logical effects are required? One effect points to a queue. Several independent effects point to separate subscriptions.
  3. Can each consumer fall behind independently? If yes, each needs its own durable progress and dead-letter policy.
  4. What is the retry identity? Stable message IDs and idempotent consumers are required wherever acknowledgements can be lost.
  5. What exactly must be ordered? Choose the narrowest aggregate or partition that protects the business rule.

Then evaluate broker capabilities: durability, retention, replay, filtering, maximum message size, throughput, partitioning, dead-letter handling, observability, and operational ownership. A feature matrix only matters after the topology is correct.

Katabench's messaging labs stage the failures hidden by architecture diagrams: a committed order whose event was never published, two dispatchers claiming the same rows, a broker outage, out-of-order aggregate messages, and an acknowledgement lost after a consumer effect. You build the outbox, lease, backoff, inbox, and deduplication that make recovery boring. The hands-on .NET labs and lab documentation describe the environment.

That is the practical answer to message queue vs event bus: model who owns the effect, give every independent consumer its own progress, and design for the delivery that arrives again.

Get new puzzles and .NET tips in your inbox

A short note when fresh kata land, plus the C# and performance tricks behind the grading. No spam, unsubscribe anytime.