Message Queues: The Post Office Pattern

📘 Chapter 8: Asynchronous Processing ⏱️ 9 min read 🏗️ Lesson 032

A message queue is a durable buffer that sits between a producer and a consumer. Think of it like a post office: you drop off a letter (message), it's stored safely, and the recipient picks it up when they're ready. Neither party needs to be available at the same time.

Core Concepts

The Five Building Blocks

  • Producer: The service that creates and sends messages to the queue
  • Queue: The durable buffer that stores messages until they're consumed
  • Message: A unit of data — could be JSON, binary, or any serialized format
  • Consumer: The service that reads messages from the queue and processes them
  • Acknowledgment (ACK): The consumer tells the queue "I've processed this message successfully — delete it"

If the consumer crashes before sending an ACK, the message becomes visible again for reprocessing. This is how queues provide at-least-once delivery.

How It Works

Message Queue Architecture Producer (Order Service) publish Message Queue msg 1 msg 2 msg 3 msg 4 ... consume Consumer (Email Service) ACK ✓ Dead Letter Queue Failed messages land here after N retries Why Queues? ✓ Decouples producer & consumer ✓ Absorbs traffic spikes ✓ Survives consumer crashes ✓ Enables retry logic ✓ Scale consumers independently
Figure 1: Producer publishes messages to a durable queue. Consumer processes them and ACKs. Failed messages go to the Dead Letter Queue.

Delivery Guarantees

Three Levels of Delivery

  • At-most-once: Message delivered 0 or 1 time. Fast but lossy. If the consumer crashes mid-processing, the message is gone. Used for: metrics, logs where occasional loss is OK.
  • At-least-once: Message delivered 1 or more times. If the consumer crashes, the message is redelivered. Most common choice. Requires idempotent consumers.
  • Exactly-once: Message delivered exactly 1 time. The holy grail — but nearly impossible in distributed systems. Requires coordination between the queue and the consumer's state (e.g., Kafka transactions + idempotent writes).

Why exactly-once is hard: After processing a message, the consumer must both (a) commit its work and (b) ACK the message. If it crashes between those two steps, you get a duplicate. True exactly-once requires atomic transactions spanning both systems.

Idempotency: The Essential Companion

Why Consumers Must Be Idempotent

With at-least-once delivery, your consumer will see the same message twice eventually. An idempotent operation produces the same result whether executed once or many times:

  • SET balance = 100 — safe to repeat
  • INCREMENT balance BY 50 — doubles the credit on replay!
  • INSERT ... ON CONFLICT DO NOTHING — using a unique message ID
  • UPDATE orders SET status='shipped' WHERE id=123 AND status='paid' — state guard

Pattern: Include a unique idempotency_key in each message. Before processing, check if you've already handled that key. If yes, skip.

Popular Message Queue Systems

Comparison: RabbitMQ vs Kafka vs SQS

Feature RabbitMQ Apache Kafka Amazon SQS
Model Traditional queue Distributed log Managed queue
Ordering Per-queue FIFO Per-partition FIFO Best-effort (FIFO available)
Throughput ~50K msg/s ~1M+ msg/s ~3K msg/s (standard)
Retention Until consumed Configurable (days/forever) Up to 14 days
Replay No (consumed = gone) Yes (seek to any offset) No
Complexity Medium (self-hosted) High (ZooKeeper/KRaft) Low (fully managed)
Best for Task routing, RPC Event streaming, logs Simple async on AWS

Dead Letter Queues (DLQ)

Where Failed Messages Go to Rest

When a message fails processing repeatedly (e.g., 3 retries), it's moved to a Dead Letter Queue instead of being retried forever. This prevents a single "poison pill" message from blocking the entire queue.

  • Investigation: Engineers inspect DLQ messages to find bugs
  • Replay: After fixing the bug, messages can be moved back to the main queue
  • Alerting: DLQ depth > 0 should trigger an alert — something is failing

🏢 Real-World: Shopify on Black Friday

Shopify handles 10x normal traffic during Black Friday/Cyber Monday. Their strategy:

  • Checkout requests are processed synchronously (users need confirmation)
  • Everything else — inventory updates, analytics, email confirmations, webhook deliveries — goes through message queues
  • During peak, the queue depth grows (messages buffer) but nothing is lost
  • Workers process the backlog over the following minutes/hours
  • Result: The storefront stays responsive even at 10x load because the queue absorbs the spike

Without queues, every downstream service would need to scale to 10x capacity for a few hours — enormously expensive. With queues, downstream processes at its normal rate while the queue acts as a shock absorber.

🏢 Real-World: LinkedIn & Kafka — 7 Trillion Messages/Day

LinkedIn invented Apache Kafka in 2011 to solve their internal messaging problems:

  • Hundreds of services needed to share data: activity events, metrics, logs, change streams
  • Point-to-point connections between all services created O(n²) complexity
  • Kafka became the central nervous system: every service publishes to Kafka, every service reads from Kafka
  • Today: 7+ trillion messages per day, 100+ petabytes stored
  • Key insight: Kafka retains messages for days (a log, not a queue), so new consumers can replay history

Interactive: Producer/Consumer Simulator

Adjust production and consumption rates to see how queue depth changes over time:

Stopped
Queue Depth
0
Total Produced
0
Total Consumed
0
Queue:
0 / 100
Adjust the rates and press Start. Watch what happens when production exceeds consumption!