Most comparisons between Kafka and RabbitMQ give you a table. Protocol, throughput, message ordering, delivery guarantees - and then you're supposed to "choose the right tool for the job." This doesn't help.
In RabbitMQ, message is gone after it's consumed. In Kafka, it stays - for hours, days, or indefinitely. That one difference drives almost every other tradeoff between the two.
RabbitMQ is a message broker. A producer sends a message. RabbitMQ routes it to one or more consumers via exchanges and queues. Once a consumer acknowledges the message, it's gone. The broker's job is delivery.
Kafka is a distributed log. A producer appends an event to a topic. The event stays there - for hours, days, or indefinitely, depending on your retention config. Each consumer tracks its own position in the log (its offset). Consumers can read from any point, replay from the beginning, or pick up exactly where they left off after a crash.
That's the whole comparison. Everything else - throughput numbers, ecosystem, operational weight - flows from this.
Do you need replay? If yes, use Kafka. RabbitMQ deletes messages after consumption. Kafka keeps them. Replay matters when you're rebuilding a read model, debugging production events, onboarding a new service that needs historical data, or recovering from a processing bug.
Do you need to fan out to many independent consumers? Kafka handles this naturally - each consumer reads the log at its own pace with no extra configuration. With RabbitMQ, fan-out means either a fanout exchange (all queues get everything) or separate queues per consumer. Each copy is stored independently.
Do you need complex routing? RabbitMQ's exchange types - direct, topic, headers, fanout - give you flexible per-message routing with very little code. Kafka routes by topic only. Fine-grained routing in Kafka ends up in application code or a stream processing layer.
What throughput do you actually need? RabbitMQ handles 20–50K messages/second per node comfortably. Kafka handles 1M+ per broker. If you're not at serious scale, RabbitMQ's throughput is more than enough, and you'll never notice the difference.
Do you just need a job queue? If you're sending emails, processing file uploads, or running background tasks in a Node.js app - use BullMQ. It's Redis-backed, handles retries, delays, and rate limiting out of the box, and adds no new infrastructure if you're already on Redis. Reaching for Kafka or RabbitMQ for this is genuine overkill.
KafkaJS used to be the standard Node.js client, but it hasn't been maintained since 2023. The current recommendation is , which wraps the battle-tested librdkafka C library and ships with a KafkaJS-compatible API mode:
Occasional notes on software, tools, and things I learn. No spam.
Unsubscribe anytime.
@confluentinc/kafka-javascriptimport { Kafka } from "@confluentinc/kafka-javascript";
const kafka = new Kafka({
kafkaJS: { brokers: ["localhost:9092"] },
});Producer:
const producer = kafka.producer();
await producer.connect();
await producer.send({
topic: "orders",
messages: [
{
key: "order-123",
value: JSON.stringify({ orderId: "123", amount: 59.99 }),
},
],
});
await producer.disconnect();Consumer:
const consumer = kafka.consumer({ kafkaJS: { groupId: "order-service" } });
await consumer.connect();
await consumer.subscribe({ topic: "orders", fromBeginning: false });
await consumer.run({
eachMessage: async ({ partition, message }) => {
const order = JSON.parse(message.value!.toString());
// process order
},
});The groupId is important: consumers in the same group split the partitions between them (parallel processing). Consumers in different groups each get all messages independently (fan-out).
Using amqplib, which is already well-established in the Node.js ecosystem:
Producer:
import { connect } from "amqplib";
const connection = await connect("amqp://localhost");
const channel = await connection.createChannel();
await channel.assertExchange("orders", "topic", { durable: true });
channel.publish(
"orders",
"order.placed",
Buffer.from(JSON.stringify({ orderId: "123", amount: 59.99 })),
{ persistent: true },
);Consumer:
const q = await channel.assertQueue("", { exclusive: true });
await channel.bindQueue(q.queue, "orders", "order.*");
channel.consume(q.queue, (msg) => {
if (!msg) return;
const order = JSON.parse(msg.content.toString());
// process order
channel.ack(msg);
});The routing key "order.*" matches order.placed, order.cancelled, order.refunded - anything in that namespace. This is RabbitMQ's topic exchange doing work that Kafka would push into application code. For more on how exchanges and queues fit together, this breakdown covers the routing model. If you need request-response over RabbitMQ, the RPC pattern with amqplib shows the full correlation ID approach.
RabbitMQ runs as a single node for development and small production workloads. One Docker container, one amqp:// URL. Clustering is optional and well-documented. That simplicity is a real advantage - it's why RabbitMQ is often the default choice for teams that need messaging but not a data platform.
Kafka has historically required running ZooKeeper alongside it - two distributed systems to manage. Kafka 4.0 (released March 2025) removed ZooKeeper entirely. Kafka now runs in KRaft mode only, which manages cluster metadata internally. This is a meaningful simplification, but you're still operating a multi-broker cluster in production. In practice, most teams running Kafka at scale use a managed service: Confluent Cloud, Amazon MSK, or Redpanda Cloud.
Some architectures use both. Kafka as the event log - the source of truth for what happened, with replay and fan-out. RabbitMQ for task routing - specific jobs to specific workers, with routing logic and acks. The two can complement each other when a system has genuinely different requirements in different areas.
This is not a starting point. It's a pattern that emerges at scale, after you've outgrown one tool for specific workloads.
Use RabbitMQ when:
Use Kafka when:
Use BullMQ when:
For most backend services: start with RabbitMQ. It's easier to run, easier to reason about, and fits typical service-to-service communication well. Add Kafka when you hit a concrete requirement it can't meet - usually replay or scale.