Siddhant Deval
Siddhant Deval
backend14 min read

Queues vs Logs: The Architecture Decision That Changes Everything

A message queue and a commit log are architecturally opposite primitives. Queues delete messages on consumption — they are for task dispatch. Logs retain events indefinitely — they are for event streaming and independent consumer replay. Treating them as interchangeable is an architectural mistake that forces a full rewrite at scale.

Queues vs Logs: The Architecture Decision That Changes Everything

The engineering team that builds the notification system using RabbitMQ ships quickly, the product works, and eighteen months later they need to add a real-time analytics pipeline that replays the last 90 days of events to train a recommendation model. They discover that RabbitMQ deleted every message the moment the original consumer acknowledged it. The events are gone. The only record of what happened is the application database, which was not designed to be an event log. The rewrite takes six weeks. This is not a RabbitMQ failure — it is a model mismatch. The team needed a log. They built with a queue.

A message is a fact about the world — and the single most consequential decision in any messaging architecture is what happens to that fact after the first consumer reads it.

Architectural Note

Series positioning: This is Part 2 of Distributed Messaging Systems. Part 1 established why synchronous coupling fails and what messaging enables. This article establishes the architectural divide between queues and logs — a decision that governs which broker you choose and cannot be reversed without a full rewrite. Part 3 dives into Kafka internals; Part 4 into RabbitMQ and AMQP.


1. Two Primitives, Two Mental Models

1.1 The Queue: Task Dispatch

A message queue operates on a simple contract: a message exists until exactly one consumer acknowledges it, then it is deleted. The queue is a work distributor — tasks flow in, workers compete to claim them, and the queue tracks only which tasks are outstanding.

Key properties of the queue model:

Property Behaviour
Delivery Each message delivered to exactly one consumer (competing consumers)
After ack Message permanently deleted
Replay Not possible — deleted messages cannot be re-consumed
Consumer independence Consumers share a single queue position — they compete
Backpressure Queue depth grows; consumers slow down the drain

1.2 The Log: Event Streaming

A commit log operates on the opposite contract: messages are appended to a sequential, durable log and never deleted on consumption. Each consumer group maintains its own offset cursor — a pointer to the position it has read up to. Two consumer groups reading the same log are fully independent.

Key properties of the log model:

Property Behaviour
Delivery Each consumer group gets every message
After read Message retained — log is append-only
Replay Possible — reset offset to re-consume from any point
Consumer independence Each group has its own cursor; groups do not interfere
Backpressure Consumer lag measured in offset distance; broker unaffected

2. The Offset Model: Why Consumer Independence Matters

2.1 What an Offset Is

An offset is a 64-bit integer — the position of a message within a partition. The log is immutable; offsets are monotonically increasing. A consumer group's progress is entirely represented by a single number per partition: the committed offset, which is the next message it will read.

TYPESCRIPT
// ✅ Kafka consumer: offset cursor is per consumer group, per partition
const consumer = kafka.consumer({ groupId: 'analytics-service' })
await consumer.subscribe({ topic: 'orders', fromBeginning: false })

await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    const offset = message.offset  // "127" — position in partition
    const value  = message.value?.toString()

    await analyticsDb.insert(JSON.parse(value!))

    // Offset committed after processing — not before
    // Committing before processing = at-most-once (data loss on crash)
    await consumer.commitOffsets([{
      topic,
      partition,
      offset: (BigInt(offset) + 1n).toString(),
    }])
  }
})
Crucial Requirement

Commit the offset after processing succeeds, not before. Committing before guarantees at-most-once delivery — if the process crashes after committing but before writing to the database, the event is lost permanently. The Kafka offset model makes at-least-once the default; idempotent processing makes it effectively-once.

2.2 Independent Consumer Groups

The same log, consumed twice, independently:

TYPESCRIPT
// ✅ Two completely independent consumer groups reading the same topic
// Adding a new consumer group requires zero changes to the producer or existing consumers

const analyticsConsumer      = kafka.consumer({ groupId: 'analytics-service' })
const notificationsConsumer  = kafka.consumer({ groupId: 'notifications-service' })
const auditConsumer          = kafka.consumer({ groupId: 'audit-service' }) // added 6 months later

// All three subscribe to the same topic
// All three maintain independent offsets
// Analytics at offset 12,400 — Notifications at offset 12,399 — Audit at offset 0 (replaying from start)

This is impossible with a queue. Once Worker 1 acknowledges a message, Worker 2 cannot read it. Consumer group independence is a log property.

Mental Model Check

Think of a log as a DVD and a queue as a cinema ticket. A DVD can be watched by any number of viewers, each starting and stopping independently, rewinding at will. A cinema ticket grants exactly one seat for one showing — once used, the seat is gone.


3. The Decision Matrix

Choosing the wrong primitive is not a performance problem you can tune away. It is an architectural mismatch that forces a rewrite. Use this matrix to make the decision before you write any code:

Criterion Use a Queue (RabbitMQ / SQS / Azure Service Bus) Use a Log (Kafka / Kinesis / AWS MSK)
How many consumers need the event? Exactly one (task dispatch) Multiple independent consumers
Do you need replay? No — task either runs or is retried Yes — reprocess, backfill, add new consumers later
Event retention Delete after ack Hours to years (configurable)
Throughput requirements Moderate (< 100K msg/s typical) Very high (millions msg/s per partition)
Consumer routing complexity Complex (topic exchanges, header routing, fan-out via bindings) Simple (partition key determines assignment)
Latency sensitivity Lower end-to-end latency (no replication wait) Higher latency at very low throughput (batch accumulation)
Operational overhead Lower — managed services widely available Higher — partition planning, replication factor, consumer lag monitoring

3.1 The Replay Use Cases That Force Logs

These requirements all mandate a log. If you discover any of them after building on a queue, you face a rewrite:

TYPESCRIPT
// These are impossible on a queue — only possible on a log

// 1. Replay to train a new ML model
await analyticsConsumer.seek({ topic: 'orders', partition: 0, offset: '0' })
// Reprocess all 6 months of order events from the beginning

// 2. Add a new microservice and backfill its read model
const inventoryConsumer = kafka.consumer({ groupId: 'inventory-read-model' })
// Starts at beginning — catches up independently while other consumers run at head

// 3. Debug a production incident by replaying a 2-hour window
await consumer.seek({ topic: 'payments', partition: 0, offset: incidentStartOffset })
// Replay 2 hours of events through the debug consumer to reproduce the issue

// 4. Time-travel: consumer crashed for 4 days — no data loss
// Queue: messages expired or stack depth unmanageable
// Log: consumer resumes from its committed offset — all events retained
Performance / Safety Warning

The most common architectural mistake is choosing a queue for simplicity and then adding fan-out by publishing the same message to multiple queues from the producer. This re-creates the tight coupling messaging was supposed to eliminate — now the producer must know every consumer and explicitly route to each one. A log decouples this entirely: producers are unaware of consumers.


4. Hybrid Architectures: When to Use Both

Real systems use both primitives — each for the role it is designed for:

  • Kafka (log) for the primary event stream — multiple consumers, replay, audit, analytics
  • RabbitMQ (queue) for task dispatch within a single bounded context — fraud alert workers compete to process each alert exactly once
Pro Tip & Optimization

A useful heuristic: if you're thinking "every consumer needs every event," reach for a log. If you're thinking "I need exactly one worker to process each task," reach for a queue. Most production systems need both.


5. Managed Primitives: Cloud Equivalents

Understanding the abstract model lets you map it to managed cloud services:

Model Self-Hosted AWS Google Cloud Azure
Queue RabbitMQ SQS Cloud Tasks Service Bus Queues
Log Kafka Kinesis / MSK Pub/Sub Event Hubs
Hybrid Pub/Sub SNS → SQS Pub/Sub Service Bus Topics
Architectural Note

AWS SNS + SQS fan-out is the managed equivalent of a RabbitMQ topic exchange: SNS broadcasts to multiple SQS queues, each with their own consumers. It looks like a log (fan-out to multiple subscribers) but each SQS queue is still a destructive queue — messages are deleted on ack. This means individual subscribers can replay within their own queue's visibility window (default 30 seconds), but you cannot add a new subscriber and replay historical events. It is fan-out without replayability.


6. The Retention Model: How Long Does the Log Remember?

Kafka and Kinesis retain messages by time or size, not by consumption:

BASH
# Kafka topic retention configuration
kafka-configs.sh --alter --topic orders \
  --add-config 'retention.ms=604800000'  # 7 days (not "until consumed")
  # OR
  --add-config 'retention.bytes=107374182400'  # 100 GB per partition

# Kinesis shard retention
aws kinesis increase-stream-retention-period \
  --stream-name orders \
  --retention-period-hours 168  # 7 days (max without extended retention: 8760 = 1 year)

This has a critical implication: a consumer that falls behind and does not catch up within the retention window permanently loses access to the unread messages. The log is not unlimited storage — it is a sliding window.

Performance / Safety Warning

Consumer lag is not free on a log. A consumer group that is thousands of hours behind is consuming disk on every broker in the replication factor. Monitor consumer lag (kafka-consumer-groups.sh --describe) actively. A consumer that is offline for longer than the retention period will fail on restart with OffsetOutOfRangeError — its committed offset no longer exists in the log.

TYPESCRIPT
// ✅ Handle OffsetOutOfRangeError — consumer was offline longer than retention
const consumer = kafka.consumer({
  groupId: 'analytics-service',
  // On out-of-range: jump to earliest available offset (data gap accepted)
  // Alternative: 'latest' — skip the gap and start from now (data loss accepted)
})

consumer.on(consumer.events.GROUP_JOIN, async () => {
  // Emit metric: consumer group caught up / offset delta to head
})

Summary

Concept Rule
Queue vs Log Queues (AMQP) delete messages on ack — they are for task dispatch; logs (Kafka) retain forever — they are for event streaming.
Consumer independence Consumer independence is a log property, not a queue property: separate consumer groups each maintain their own offset cursor.
Decision is irreversible Choosing the wrong primitive forces an architectural rewrite later; the decision is irreversible at scale.

What's Next

Part 3: Kafka Internals — Partitions, Leaders, and the Commit Log dives below the offset model into how Kafka actually stores and replicates data: the write path from producer.send() through leader election, ISR replication, and acks semantics. Part 4 covers the RabbitMQ and AMQP model in equivalent depth — both are independent reads grounded in the queue vs log distinction established here.

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Kafka#RabbitMQ#Message Queue#Commit Log#Event Streaming#Distributed Systems#Backend
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.