# RabbitMQ vs Kafka for .NET Applications

> RabbitMQ deletes messages once consumed; Kafka keeps them and moves a cursor. That single design decision explains almost every difference between them: replay, fan-out, ordering, routing, and scaling. Understand it and the choice for your .NET system mostly makes itself.

Published: 2026-07-04. Last updated: 2026-07-14. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/rabbitmq-vs-kafka-dotnet

**RabbitMQ deletes messages, Kafka keeps them.**
A queue tracks messages and destroys them on acknowledgment; a log appends messages, retains them for a configured time, and consumers just move a cursor.
Pick RabbitMQ for commands, jobs, and flexible routing; pick Kafka for event streams that multiple systems consume independently, replay, and high sustained throughput.
Almost every practical difference between the two follows from that first line.

Most "RabbitMQ vs Kafka" comparisons eventually collapse into feature bingo: this one has routing, that one has replay, this one has priorities, that one has partitions.
Skip the bingo.
Let's derive everything from the root decision instead.

## The Root Difference, Precisely

**RabbitMQ** is a smart broker managing queues.
A message is published to an exchange, routed into one or more queues, delivered to a consumer, and on acknowledgment it is **gone**.
The broker's job is tracking the state of every individual message: delivered, unacked, requeued, dead-lettered.

**Kafka** is a distributed, partitioned, append-only log.
A message is appended to a partition and stays there until retention expires (hours, days, or forever), consumed or not.
The broker tracks almost nothing per message; each consumer group remembers one number per partition: its **offset**.

One model destroys state as work completes.
The other never mutates and lets readers keep their own bookmarks.

![RabbitMQ routes a message through an exchange into a queue where a consumer acks it and the message is deleted, while Kafka appends to a retained partitioned log and each consumer group tracks its own offset](https://milanjovanovic.tech/blogs/articles/rabbitmq-vs-kafka-dotnet/queue-vs-log.png)

Now watch the consequences fall out.

## Consequence 1: Replay

Kafka: move your offset backwards and re-read yesterday.
Rebuild a projection, backfill a new service, replay production traffic into a test consumer.
The data is just *there*, sitting in the log.

RabbitMQ: the message you acked an hour ago no longer exists.
Replay requires you to have stored the messages somewhere else first, or to republish from the source system.

If your architecture leans on event history (event sourcing adjacency, rebuilding read models, audit-grade streams), this consequence alone decides the question.
Queues have no history because deleting history is what a queue *is*.

## Consequence 2: Fan-Out

Ten services want every `OrderPlaced` event.

RabbitMQ: the broker copies the message into ten queues (a **fanout exchange** bound ten times).
Ten copies, ten sets of delivery state, paid at publish time.

Kafka: nothing is copied.
Ten consumer groups hold ten offsets against the same partition data.
An eleventh subscriber tomorrow costs one more integer per partition, and it can start from the beginning of retention.
This is why fan-out-heavy event-driven architectures gravitate to logs: subscribers are free, and late subscribers get history.

![RabbitMQ fan-out copies an OrderPlaced event into three separate queues, while Kafka keeps a single copy in the topic and three consumer groups each track their own offset against it](https://milanjovanovic.tech/blogs/articles/rabbitmq-vs-kafka-dotnet/fan-out.png)

RabbitMQ fan-out works fine at normal scale, to be clear.
But the asymmetry is structural: copies versus cursors.

## Consequence 3: Competing Consumers and Ordering

Here the queue wins one back.

RabbitMQ: point five consumers at one queue and the broker deals messages out one at a time, tracking each individually.
Scale to six consumers mid-burst and the sixth starts pulling work immediately.
Per-message acknowledgment means a slow message does not block its neighbors, and a failed one can be individually requeued, delayed, or dead-lettered.

Kafka: parallelism is **partitions**.
A partition is consumed by exactly one consumer in a group, in order, because an ordered log cannot be dealt out message-by-message without ceasing to be ordered.
Your consumer count is capped by partition count, rebalances briefly pause consumption when membership changes, and one slow message holds up everything behind it in its partition (head-of-line blocking).

The flip side: Kafka gives you **per-key ordering for free**.
Same key, same partition, guaranteed order, at any scale.
In RabbitMQ, ordering holds only within a queue consumed by a single consumer; the moment you add competing consumers, redeliveries can interleave and order is gone.
That is the trade I unpacked in [**solving message ordering from first principles**](https://milanjovanovic.tech/blog/solving-message-ordering-from-first-principles): queues optimize for fair work distribution, logs for ordered streams, and you cannot fully have both at once.

## Consequence 4: Routing and Broker Smarts

Because RabbitMQ tracks every message anyway, it can afford to be clever per message: exchanges route on keys, patterns, and headers; messages carry TTLs and priorities; unroutable messages can bounce to alternate exchanges.
Even delayed and scheduled delivery is possible, though not out of the box: the **rabbitmq-delayed-message-exchange** community plugin is a separate download you install and enable on the broker.
The broker is a programmable post office.

Kafka's broker is deliberately dumb: append bytes, serve byte ranges, replicate.
"Routing" is choosing a topic and a key at the producer.
Filtering happens in consumers or stream processors.
Delay, priority, and per-message TTL simply do not exist, because implementing them would mean mutating a log whose entire value is that it does not mutate.

If your system needs rich broker-side routing (per-tenant queues, priority lanes, selective subscriptions), RabbitMQ hands it to you.
On Kafka you rebuild those as topics-plus-convention, and some (priority, per-message delay) never stop being awkward.

## Consequence 5: Throughput

Kafka's numbers come from the log structure, not magic: sequential appends, batched and compressed reads, zero per-message broker state, and horizontal spread across partitions.
Kafka is designed for high sustained sequential throughput through partitioning and batching.

RabbitMQ is not slow (tens of thousands per second per node is normal, more with tuning), but per-message tracking, routing decisions, and deletion have a floor cost that log appends do not.
At modest volumes, **both may have ample capacity**, so throughput should not outweigh delivery semantics or operational fit.
Be honest about your volume before letting throughput charts pick your infrastructure.

## Consequence 6: Failure Handling

A work-distribution system needs per-message failure treatment: retry this one, delay that one, quarantine the poison one.
RabbitMQ's per-message state makes all of it native: nack with requeue, retry topologies with backoff, dead-letter exchanges, delivery counts.

Kafka cannot treat one message specially without blocking its partition, so failure handling moves into *your* code: catch, produce to a `retry` or `dlq` topic, commit, and keep moving.
It works, every serious Kafka shop does it, but you are assembling from parts what the queue gives you assembled.
Either way, redelivery exists in both worlds, so [**idempotent consumers**](https://milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it) are mandatory in both.

## The .NET Angle

Client maturity is a wash: `RabbitMQ.Client` (now fully async in v7) and `Confluent.Kafka` are both first-rate, and I have getting-started guides for each (**RabbitMQ**, **Kafka**).

The framework layer tilts slightly RabbitMQ-ward: MassTransit, NServiceBus, Wolverine, and Rebus all treat queues as the native abstraction, with sagas, retries, and outbox integration designed around per-message acknowledgment.
Kafka support exists (MassTransit riders, Confluent's ecosystem) but consumer ergonomics, especially error handling, stay closer to the metal.

Operationally: modern Kafka (KRaft, no ZooKeeper) is far easier than its reputation, but a well-run Kafka cluster is still more system than a well-run RabbitMQ node, and both lose to a managed service.
If your team is small, weight this heavily.

## Side-by-Side Comparison

Here's how the queue and the log stack up, consequence by consequence:

|  | RabbitMQ | Kafka |
| --- | --- | --- |
| Core model | Queue: messages deleted on acknowledgment | Log: messages retained, consumers track offsets |
| Replay | Gone once acked | Move the offset back and re-read |
| Fan-out | Broker copies the message into each bound queue | One copy, one offset per consumer group |
| Ordering | Only within a queue with a single consumer | Per-key ordering within a partition |
| Scaling consumers | Add consumers to a queue at any time | Capped by partition count |
| Routing | Broker-side exchanges, TTLs, priorities | Topic and key at the producer; filtering in consumers |
| Failure handling | Native retries, delays, dead-letter exchanges | Retry and DLQ topics built in your own code |
| Throughput | Tens of thousands per second per node | High sustained throughput via partitioning and batching |
| Best for | Commands, jobs, task distribution | Event streams, replay, multiple independent readers |

## How Do You Choose?

Ask what the messages *are*:

- **Commands and jobs** ("resize this image", "send this email"): work to be done once, deleted when done, retried individually when failed. That is a queue. **RabbitMQ.**
- **Events as facts** ("order placed", "price changed"): a stream multiple systems read independently, possibly including systems that do not exist yet. That is a log. **Kafka.**
- **Both**, which is common: many organizations run RabbitMQ for task distribution and Kafka for the event backbone, and that is architecture, not indecision.
- **Neither at your scale**: if you are adding your first queue to a monolith, a broker may be premature entirely; the [**outbox pattern**](https://milanjovanovic.tech/blog/implementing-the-outbox-pattern) plus a Postgres queue covers a surprising distance.

The mistake to avoid is buying Kafka for a job workload because of throughput charts, then hand-building acknowledgment, delay, retries, and DLQs on top of a log that resists all four.
Or the mirror image: building an event backbone on RabbitMQ and discovering that "replay last month for the new consumer" requires a time machine.

## Summary

Queues delete, logs retain.
From that root: RabbitMQ gives you per-message acknowledgment, rich routing, native retries and dead-lettering, and elastic competing consumers, at the cost of no history and copy-based fan-out.
Kafka gives you replay, cursor-based fan-out, per-key ordering, and enormous sequential throughput, at the cost of partition-bound parallelism and do-it-yourself failure handling.

Classify your messages as work or as facts, and the transport picks itself.
And when one system contains both kinds, the answer is allowed to be both brokers, each doing the job its data structure was built for.

## Frequently asked questions

### What is the main difference between RabbitMQ and Kafka?

RabbitMQ is a queue: messages are delivered to consumers and deleted on acknowledgment. Kafka is a log: messages are retained for a configured period and consumers track their own position. Almost every other difference, including replay and fan-out behavior, follows from that.

### Is Kafka faster than RabbitMQ?

For raw sequential throughput, generally yes, because appending to a partitioned log and batching reads is mechanically cheaper than per-message routing, tracking, and deletion. For per-message latency at modest volumes both are fast enough that the operational differences matter more.

### Can Kafka replace RabbitMQ as a work queue?

It can, but awkwardly. Kafka has no per-message acknowledgment, no built-in delayed redelivery, no priority, and no native dead-letter queue, so competing-consumer job processing takes more work. If your workload is jobs and commands, a queue is the better fit.

### Which should a .NET team pick by default?

If you need task distribution, RPC-style commands, and flexible routing, pick RabbitMQ. If you need event streams that multiple systems consume independently, replay, or very high sustained throughput, pick Kafka. Many organizations legitimately run both for different jobs.
