Field guide · message brokers

One mental model,
five brokers.

Topics, consumer groups, workers, ordering, retries — the same idea wears a different name in every broker. Here are Kafka, NSQ, NATS (JetStream), RabbitMQ, and AWS lined up so the mental model carries across.

verifiedNSQ delivery is push, not pull — a consumer advertises a RDY count and nsqd pushes up to that many messages (flow-controlled push). Comparisons that call it “pull” are wrong.

The engines, in plain English

Kafka log + partitions

A durable, replayable event log.

Producers append events to a log that sticks around; many independent teams read it at their own pace and can rewind to re-read history — think a DVR for event streams.

Reach for it when high throughput, ordered replay, and many consumers of the same data.

NSQ simple queue

A dead-simple work queue.

Push jobs in, add workers to go faster. No partitions, no ceremony — the broker spreads messages across whatever workers are connected.

Reach for it when straightforward task-spreading where you don’t need ordering or replay.

NATS JetStream

A fast bus that can grow durable.

Core NATS is lightweight fire-and-forget pub/sub; turn on JetStream and you add persistent streams, acknowledgements, and replay — same system, more guarantees.

Reach for it when low-latency messaging that may later need durability and replay.

RabbitMQ AMQP broker

The flexible router.

An exchange decides which queues each message lands in (direct / topic / fanout), with per-message retries and dead-lettering. The Swiss-army knife of routing.

Reach for it when complex routing rules and classic work-queue patterns.

EventBridge AWS · event router

AWS’s event bus and router.

Your services and SaaS apps emit events; rules match them and forward to targets — a smart switchboard for events across the whole account.

Reach for it when you need to route events between many services by rules.

SNS AWS · pub/sub fan-out

AWS’s pub/sub fan-out.

Publish once to a topic and it pushes copies to many subscribers at once — usually SQS queues, Lambdas, or webhooks.

Reach for it when one event must fan out to many subscribers immediately.

SQS AWS · managed queue

AWS’s managed queue.

It buffers messages durably until a worker pulls and deletes them; the visibility timeout and DLQ handle retries. One queue = one consumer group.

Reach for it when you need a durable buffer between producers and workers.

Lambda AWS · serverless worker

AWS’s serverless worker.

It polls SQS (or is invoked by SNS / EventBridge) and auto-scales by concurrency — no servers to run, pay per execution.

Reach for it when you want workers that scale themselves with no servers to operate.

Same idea, different name

ConceptKafkalog + partitionsNSQsimple queueNATSJetStreamRabbitMQAMQP brokerAWSEB · SNS · SQS · λ
Topic — where producers publishTopicTopicSubject (captured by a Stream)Exchange (routes to queues)EventBridge bus or SNS topic
Consumer group — independent processorConsumer GroupChannelDurable Consumer (each = 1 CG)Queue (each = 1 CG)SQS Queue (each = 1 CG)
Workers inside a groupConsumer instance in the CGWorker on a ChannelSubscribers on one pull consumer / queue groupCompeting consumers on a queueLambda execution envs (concurrency) or ECS tasks
Parallelism modelCapped by # partitions — extra workers idleUnlimited workers (no partitions)Unlimited workers (no partitions)Unlimited workers (no partitions)Unlimited; parallelism = Lambda concurrency
Partitioning / ordering unitPartition — strict order within itNonePer-subject order in a stream (strict only MaxAckPending=1)FIFO per queue (breaks on requeue / many consumers)SQS FIFO MessageGroupId
Backlog storagePartition log (retention-based)In-memory / durable nsqd queueStream (file/memory, retention-based)Queue (memory + disk when durable)SQS queue backlog
Fan-out — 1 message → many groupsMany CGs on the same topicMany channels get the same messageMany consumers/streams on the subjectFanout/topic exchange → many queuesEventBridge/SNS → many SQS queues
Delivery modelPullPushRDY-based flow controlPull or push (core NATS = push)Push (prefetch); pull via basic.getEB/SNS = push; SQS = pull (Lambda polls)
Retry behaviorOffset controls replayRequeue / timeoutNak / AckWait redeliver; MaxDeliverDLQnack/reject requeue + Dead Letter ExchangeVisibility timeout + DLQ per queue
Isolation between groupsEach CG independentEach channel independentEach consumer independent (own cursor + acks)Each queue independent (own backlog + DLX)Each queue isolates throughput, retries, DLQ

Where they diverge

DimensionKafkalog + partitionsNSQsimple queueNATSJetStreamRabbitMQAMQP brokerAWSEB · SNS · SQS · λ
Core conceptDistributed log + partitionsSimple distributed messagingSubject pub/sub + persistent streamsExchange routing + queues (AMQP)Pub/sub + routing + queue + serverless
OrderingPer-partitionBest effortPer-subject (strict only MaxAckPending=1)FIFO per queue (breaks on requeue / >1 consumer)Standard: best effort · FIFO: per MessageGroupId
Idle workers if > partitionsYes — stalledNoNoNoNo
Pull or pushPullPush (RDY flow control)Both (core = push)Push (prefetch); pull via basic.getEB/SNS = push · SQS = pull
BackpressureYes (log retention)Yes (queue backlog)Yes (retention + MaxAckPending)Yes (queue depth + prefetch)Yes (SQS backlog)
Partition-like conceptPartitionsNoneNone (subject-sharding is manual)None (consistent-hash exchange optional)FIFO MessageGroupIdpartition
Scaling methodAdd partitions or consumersAdd workersAdd subscribers / shard subjectsAdd consumers / hash exchange / quorum queuesRaise Lambda concurrency / add queues
Message replayStrong (offsets)LimitedStrong (replay by seq/time)Limited (classic once; Streams replay)DLQ reprocess / manual replay
Storage & cleanup per groupOffsets only — one shared log; renaming a group is freeOwn copy (mem + disk) — leaks; drain/delete or use #ephemeralCursor over the shared stream (like Kafka); interest/workqueue keeps msgs while wantedOwn copy per queue — delete it, or auto-delete / TTL / max-lengthOwn copy per queue — auto-expires by retention (default 4d, max 14d); DeleteQueue to remove

Worked example — where do 100 messages go?

Two layers, always: the broker fans a copy out to each consumer group, then each group splits its copy across its own workers. Pick a broker — watch which groups get a copy, and notice the split inside a group never depends on it. (RabbitMQ adds an exchange-type dial that changes which queues match.)

100 messages routing key: orders.eu.new

exchangequeuesconsumers split the copy

Topic matches patterns — orders.eu.new matches eu-orders and all-orders (orders.#), but never payments.

messages
100
eu-orders consumers
4
no partition cap — all 4 consumers share the copy
queueeu-ordersorders.eu.new
×100copy
consumer1 25consumer2 25consumer3 25consumer4 25
queueall-ordersorders.#
×100copy
consumer1 100
queuepaymentspayments.#
×0no match
1 consumer idle

100 published → 200 delivered across 2 queues (each gets its own copy). Inside eu-orders, the copy splits 25/25/25/25 across 4 consumers — that split is independent of how many queues got a copy.

Delivery & ordering — the fine print

The model covers who gets a copy and who processes it. Two details bite in production: whether message order survives, and what “delivered” actually means.

Fire-and-forget? A spectrum

NATS CoreYes — purestNo persistence, no delivery guarantee (at-most-once). Fastest — but a message is lost if nothing is listening.
NSQPartialFire-and-forget for the producer, but nsqd holds the message (memory/disk) until a consumer sends a FIN.
RabbitMQNoAcknowledgements + durable queues by default — safety and routing over raw speed.
KafkaNoA distributed append-only log — every message is written to disk first, whether or not a consumer is present.
AWS SNS/SQSNoSNS retries delivery behind the scenes; SQS stores messages durably across data centers.
NATS JetStreamNo — durablePersistence on top of Core NATS → at-least-once with acks + replay, much like Kafka.

orderingNSQ doesn’t stay FIFO with several consumers on one channel. nsqd pushes each message to a randomly-chosen ready consumer; each consumer buffers up to its RDY count and often handles them concurrently; and any failed message is re-queued and pushed again later — all three break strict order. Need order? Run one single-threaded consumer at RDY=1, or (better) make handlers idempotent and scale freely.

deliveryAt-least-once is tracked per message, not per batch. With RDY=5, nsqd pushes 5 distinct messages, each tracked on its own. A success sends FIN and the buffer refills to 5; a failure or msg_timeout sends REQ and only that one message re-queues. Every message earns at least one FIN before it’s deleted — so a crash after processing but before FIN triggers a redelivery. Build handlers to tolerate duplicates.

cleanupA group either holds a copy, or just a pointer — and only one of those leaks. Log-based brokers (Kafka, RabbitMQ Streams, NATS JetStream under limits retention) make a group a cursor over one shared copy, so a renamed or abandoned group costs nothing — its offsets simply expire. Queue-based ones (NSQ, classic RabbitMQ, SQS) give each group its own copy in memory + disk, so an orphan grows until you reclaim it. For throwaway consumers reach for the ephemeral escape hatch: NSQ #ephemeral channels (memory-only, auto-deleted on last disconnect), NATS ephemeral consumers (drop after inactivity), RabbitMQ auto-delete/exclusive queues — or a message TTL / SQS retention as the safety net.

Worked scenario — routing a ticket

An assignment must land on exactly one owner; a notification can fan out to everyone else. So you use both patterns at once — route the work to one agent, broadcast the status to the rest. The icon says which nodes are systems and which are people.

system queue · db · serviceuser agent · customer
routerassignswork queueone ownernotify · fan-outSLA streamAgentowns the ticketCustomerstatus onlyDashboardoversightDBrecord

Only the assigned agent owns the ticket record. The customer and dashboard get notification copies, not ownership. Delivery is at-least-once, so make “assign” and “notify” idempotent (dedupe on ticket id) — a redelivery mustn’t double-assign or double-notify.

Glossary

Topic
The named destination producers publish to and consumers read from. Kafka/NSQ call it a topic; NATS calls it a subject; RabbitMQ routes to it via an exchange.
Consumer group
An independent unit of consumption — its own copy of the stream and its own progress. Kafka: consumer group · NSQ: channel · RabbitMQ/AWS: a queue.
Partition
Kafka’s unit of ordering and parallelism. A topic is split into partitions; each is read in order by at most one worker per group, so max workers = partition count.
Offset
A consumer’s bookmark in a Kafka partition. Commit it to mark progress; rewind it to replay old messages.
Ordering
Whether messages arrive in the order they were sent. Usually guaranteed only within a partition (Kafka), one queue (RabbitMQ), or one SQS FIFO MessageGroupId.
FIFO
First-in, first-out — strict order. SQS FIFO queues and single RabbitMQ queues preserve it; “standard” / best-effort modes may not.
Backlog
Messages that have arrived but aren’t processed yet — the queue’s depth. A growing backlog means consumers are falling behind.
Backpressure
When the backlog grows, the system slows or buffers producers instead of dropping data — pressure pushing back upstream.
Fan-out
Delivering one published message to many independent consumer groups at once.
Push
The broker sends messages to consumers as they arrive (RabbitMQ, NSQ, SNS). Flow is capped by prefetch / RDY so a consumer isn’t flooded.
Pull
Consumers ask for messages when they’re ready (Kafka, SQS, JetStream pull) — they set their own pace.
Fire-and-forget
Send a message and don’t wait for confirmation or store it — fast, but with no delivery guarantee and no replay. Core NATS pub/sub works this way; add JetStream when you need durability.
RDY
NSQ’s flow-control signal: a consumer advertises how many messages it can take, and nsqd pushes up to that many.
Prefetch
RabbitMQ’s cap on how many unacknowledged messages a consumer may hold at once — its backpressure knob.
Dead letter
Where a message goes after too many failed deliveries, so it doesn’t block the queue. AWS/others: a DLQ; RabbitMQ: a dead-letter exchange (DLX).
Visibility timeout
SQS’s retry window: a received message is hidden for a set time; if it isn’t deleted (acked) in time, it reappears for redelivery.
Replay
Re-reading past messages. Log-based systems (Kafka offsets, JetStream by seq/time) replay easily; once-delivered queues generally can’t.
Stream
NATS’ persistent layer: a stream stores the messages on its subjects, giving consumers durability, acks, and replay.
Ack
Acknowledgement — a consumer confirming it processed a message. A Nak or timeout (AckWait) triggers redelivery; MaxDeliver caps the attempts.
At-least-once
A delivery guarantee: a message is redelivered until the consumer acknowledges it, so it may arrive more than once (a crash right before the ack, a timeout). The default for durable brokers — pair it with idempotent handlers.
At-most-once
A delivery guarantee: a message is delivered zero or one time, never retried — fast and cheap, but lost if no consumer is listening or the consumer fails. Core NATS works this way.
Idempotency
Writing a handler so processing the same message twice has the same effect as once — upsert by a stable key, or dedupe on a message id. The standard cure for at-least-once duplicates.
FIN / REQ
NSQ’s per-message acknowledgements: FIN (finish) permanently removes a message; REQ (requeue) sends it back to be delivered again. A message with no FIN before its msg_timeout is auto-requeued.
Ephemeral
A short-lived, auto-cleaned resource — the cure for an orphaned queue that would otherwise leak memory + disk. An NSQ channel/topic suffixed #ephemeral is memory-only and deleted when its last consumer disconnects; NATS ephemeral consumers vanish after an inactivity threshold; RabbitMQ auto-delete / exclusive queues drop when their consumer or connection goes.
Exchange
RabbitMQ’s router: producers publish to an exchange, which copies each message to the queues bound to it (see exchange types).
Direct / topic / fanout
RabbitMQ exchange types — how an exchange picks target queues. Direct: deliver to queues whose binding key exactly matches the message’s routing key. Topic: match routing keys against wildcard patterns (e.g. orders.*.eu). Fanout: ignore keys entirely and copy to every bound queue.
Queue
A buffer holding messages for competing consumers to share. In RabbitMQ/SQS the queue is also the boundary of one independent consumer group.
Concurrency
How many workers run at once. In AWS this is Lambda concurrency — more concurrency, more messages processed in parallel.
Built by Pangaea Labs

NATS rows describe JetStream (the persistent tier); core NATS is at-most-once pub/sub with no groups, backlog, or replay. RabbitMQ rows assume classic queues unless noted.