Message Queues & Event Streaming
Message queues decouple producers from consumers: a producer publishes a message and moves on, while consumers process it at their own pace. Queues guarantee at-least-once delivery, which forces idempotent consumers, and streams like Kafka add durable, replayable, ordered logs. Lags, ordering and poison messages are the operational realities of every queue-backed system.
What is this?
A message queue is a broker that accepts messages from producers, stores them, and hands them to consumers, so the two never meet synchronously. Point-to-point queues deliver each message to one consumer; pub/sub topics broadcast to many; streaming platforms like Kafka add durable, ordered, replayable logs partitioned for parallelism. Brokers deliver with at-least-once semantics by default, so consumers must be idempotent, and failures route to a dead-letter queue (DLQ) instead of blocking the pipeline.
Why do you need it?
- Asynchronous decoupling lets a slow or briefly-down consumer never block the producer's request path.
- Queues absorb bursts: when traffic spikes, messages buffer and drain at the consumer's speed instead of dropping work.
- Consumers scale independently — add workers to process faster without touching the producer.
- Retries and DLQs make failures visible and recoverable instead of silently lost.
- Event streams preserve a durable, replayable history that can rebuild state, feed analytics, or power new consumers later.
Mental model
A restaurant kitchen and waiters shouting orders at each other
A ticket rail with numbered orders
The cook drops a ticket on the rail and moves on; any free waiter takes it. If a ticket is illegible it goes to a special bin (the DLQ) rather than blocking the whole kitchen. Customers can also subscribe to the daily specials board (pub/sub) and react independently.
How it works
- A producer sends a message to a queue or topic on the broker; the send returns once the broker has accepted it.
- The broker persists the message durably and delivers it to consumers, either pushing or on poll.
- Queues hand each message to one consumer in the group; topics fan out a copy to every subscribed consumer group.
- Consumers acknowledge after processing; unacknowledged messages are redelivered, giving at-least-once semantics.
- Streams additionally divide data into partitions; messages with the same key go to the same partition, preserving per-key order.
- Messages that exhaust their retry budget land in a dead-letter queue for inspection and manual replay.
A producer publishes once; the broker persists and fans out; one consumer per partition in a group processes it.
Key commands
kafka-topics.sh --create --topic order.created --partitions 3 --replication-factor 1Create a topic with three partitions for parallel consumption.
Created topic order.created.
kafka-console-producer.sh --topic order.created --property key.separator=:Send test messages interactively with explicit keys.
kafka-console-consumer.sh --topic order.created --group payments --from-beginningConsume from the beginning of the topic in the payments group.
kafka-consumer-groups.sh --describe --group paymentsShow lag per partition — how far consumers are behind the latest messages.
TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG\norder.created 0 1234 1240 6
kafka-topics.sh --describe --topic order.createdInspect partition count, replication and leaders for a topic.
docker compose exec kafka kafka-consumer-groups.sh --group payments --reset-offsets --to-earliest --executeReset consumer offsets to replay messages (use carefully in production).
Examples
A Kafka producer and consumer for order events (KafkaJS)
import { Kafka } from "kafkajs"; const kafka = new Kafka({ clientId: "order-service", brokers: ["kafka-1:9092", "kafka-2:9092"],}); // Producer: publish the fact that an order was createdconst producer = kafka.producer();await producer.connect();await producer.send({ topic: "order.created", messages: [ { key: "order-123", value: JSON.stringify({ orderId: 123, total: 49.99 }) }, ],}); // Consumer: a payments worker in a consumer groupconst consumer = kafka.consumer({ groupId: "payments" });await consumer.connect();await consumer.subscribe({ topic: "order.created" });await consumer.run({ eachMessage: async ({ topic, partition, message }) => { const event = JSON.parse(message.value.toString()); // Idempotent: charging the same orderId twice is a no-op await chargePayment(event.orderId, event.total); console.log("paid", event.orderId, "from partition", partition); },});The producer keys the message by orderId, so every message for order-123 lands in the same partition and is processed in order. The consumer joins the 'payments' group, so if two workers run, each owns different partitions. chargePayment is written to be idempotent — charging the same orderId twice is harmless — because Kafka redelivers messages when a consumer crashes after processing but before committing its offset.
In production
- Order pipelines publish order.created and separate workers handle payment, inventory, emails and analytics, each scaling on its own.
- Change data capture (Debezium) streams database row changes into Kafka so search indexes and read models stay in sync with the source of truth.
- Analytics platforms buffer millions of events through a stream, then batch them into warehouses at controlled rates.
- Integrations use a retry queue plus a DLQ: messages that fail five times with external providers park in a DLQ where ops replays them after fixing the root cause.
Best practices
- Make consumers idempotent: process by a natural key (orderId, paymentId) so at-least-once redelivery is harmless.
- Use a consumer group for parallel processing and key messages by the entity whose order must be preserved.
- Monitor consumer lag and alert when it trends upward — lag is the health metric of the whole pipeline.
- Validate and schema-register messages so a bad producer cannot poison every consumer.
- Never write to the database and publish inside one transaction without an outbox; publish only after the commit, or use the transactional outbox pattern.
Common mistakes
- Assuming exactly-once delivery, then getting duplicate charges or duplicate side effects when a consumer crashes.
- Keying by timestamp or random value, which destroys the per-entity ordering you actually need.
- Ignoring consumer lag until a backlog of hours builds up and late messages become worthless.
- Reading from the beginning on every restart, re-processing the whole history instead of resuming from the committed offset.
Common issues
Duplicate email/payment despite a single user actionAt-least-once delivery redelivered a message after a consumer crash, and the consumer was not idempotent.
Key the work by a business id and deduplicate in the consumer (check an existing record before acting), or store processed ids in a dedupe store.
Consumer lag keeps growing and messages are hours lateToo few consumers or partitions for the volume, or each message does slow work (API calls, big joins).
Add partitions and consumers, parallelize the slow step, and consider batching multiple messages per unit of work.
A single consumer stops and its partition never gets processedA poison message makes the consumer error and crash repeatedly, stalling rebalance.
Route messages that fail N times to a DLQ with the error recorded, and let the partition continue past the bad message.
Debugging tips
- Check kafka-consumer-groups.sh --describe for lag: which partition and consumer is behind.
- Use a console consumer to look at raw messages and find malformed payloads (poison messages).
- Inspect the DLQ topic for the message headers that record how many times it was retried and why.
- Verify the commit strategy: committing before processing loses messages; committing after processing risks duplicates on crash — both are visible in the logs.
Interview checkpoint
Hands-on lab
- Run Kafka (single-node KRaft) in Docker
- Create a topic with multiple partitions
- Publish messages with keys and consume them in a group
- Measure consumer lag and replay messages by resetting offsets
Publish and Consume with Kafka in Docker
45 min · intermediate
Quick quiz
Which delivery semantic is the default for most message brokers?
Messages with the same key are guaranteed to be processed in order in the same Kafka partition.
Consumer lag is growing, one partition's consumer keeps crashing, and messages behind it are never processed. What is the most likely culprit?
Cheat sheet
ProducerPublishes messages to a topicConsumer groupWorkers that split the partitionsPartitionOrdered log unit; one consumer per partitionOffsetPosition of a consumer in a partitionLagOffset gap between consumer and latest messageDLQDead-letter queue for failed messageskafka-topics.sh --create --topic t --partitions 3Create a topickafka-console-producer.sh --topic tSend test messageskafka-console-consumer.sh --topic t --group gConsume in a groupkafka-consumer-groups.sh --describe --group gShow lag per partitionkafka-topics.sh --describe --topic tShow partition layout