Asynchronous Processing & Messaging

Message Queues

18 min Lesson 2 of 10

Message Queues

A message queue is a durable, ordered buffer that sits between the component that generates work (the producer) and the component that performs it (the consumer). Instead of calling each other directly, they communicate through the queue — the producer drops a message in, walks away, and the consumer picks it up whenever it is ready. This one idea unlocks a huge range of system design options.

Core insight: A message queue converts a synchronous, tightly coupled operation into an asynchronous, loosely coupled one. The producer does not wait for the consumer to finish; it does not even need to know the consumer exists.

The Producer / Consumer Model

Every queue-based system has three moving parts:

  • Producer — creates a message and sends it to a named queue. The message encodes a unit of work: an order to process, an image to resize, an email to send.
  • Queue (Broker) — stores the message durably until it is delivered. The broker is the middleman; it accepts messages from producers and hands them to consumers. Popular brokers include RabbitMQ, Amazon SQS, Azure Service Bus, and ActiveMQ.
  • Consumer — polls or receives messages from the queue and processes them. After successful processing it acknowledges (acks) the message so the broker can remove it.
Producer / Consumer Model with Message Queue Producer Order Service e.g. API Server Message Queue (Broker) msg 1 msg 2 msg 3 durable, ordered storage RabbitMQ / SQS / ActiveMQ Consumer 1 Email Worker Consumer 2 Invoice Worker publish consume consume ack
A single producer publishes messages to a broker; multiple consumers pull from it independently and acknowledge on success.

Queue Semantics

Understanding the precise behavior of a queue matters when you design for correctness at scale. There are four key properties to know:

1. Ordering

Most queues provide FIFO (First-In, First-Out) ordering within a single queue or partition. Messages are delivered to consumers in the same order they were enqueued. However, when you add multiple consumers for throughput, strict global ordering is difficult to maintain — each consumer independently pops messages, so two consumers may process messages out of the original sequence relative to one another. If strict ordering matters (e.g., user account state changes), you must either use a single consumer or partition messages by a key (such as user ID) so all messages for the same user go to the same consumer.

2. Visibility Timeout (Lease-Based Delivery)

When a consumer receives a message, the broker does not delete it immediately. Instead, it hides the message for a configurable visibility timeout — say, 30 seconds. If the consumer acks within that window, the broker deletes the message permanently. If the timeout expires without an ack (because the consumer crashed), the message reappears in the queue and is re-delivered to another consumer. This is the safety net that prevents message loss on consumer failure.

Pitfall — visibility timeout too short: If your job takes 45 seconds but the visibility timeout is 30 seconds, the message will reappear mid-processing and a second consumer will start the same job. Set the timeout generously above your 99th-percentile processing time, or extend it programmatically while work is in progress.

3. Durability

A durable queue survives broker restarts. Messages are written to disk (or replicated across nodes) before the broker acknowledges the producer. This adds latency — typically a few milliseconds — but is non-negotiable for anything you cannot afford to lose. Amazon SQS stores messages across multiple Availability Zones automatically. RabbitMQ requires you to declare the queue and its messages as durable and persistent explicitly.

4. Competing Consumers

The pattern of running multiple consumer instances on the same queue is called competing consumers. It is the primary way to scale throughput: add more consumer processes and each one pulls messages independently. If you have 10,000 messages queued and spin up 20 consumer workers, the throughput scales nearly linearly — you process the backlog roughly 20× faster than a single consumer would. Cloud providers make this trivial: Auto Scaling can watch queue depth and add EC2 instances or Lambda invocations automatically.

Competing Consumers — Scaling Queue Workers Order Queue 10,000 messages pending depth monitored by autoscaler SQS / RabbitMQ Worker 1 processing msg 1 Worker 2 processing msg 2 Worker 3 processing msg 3 Worker N auto-scaled up Auto Scaler watches queue depth spawns workers on demand monitor
Competing consumers pull from the same queue in parallel; an auto-scaler adds workers when queue depth grows.

Real-World Numbers and Trade-offs

To make design decisions concrete, here are representative characteristics of popular systems:

  • Amazon SQS Standard queue: nearly unlimited throughput, at-least-once delivery, best-effort ordering. Maximum message size 256 KB. Visibility timeout up to 12 hours. Retention up to 14 days.
  • Amazon SQS FIFO queue: strict ordering and exactly-once processing within a message group. Capped at 3,000 messages/sec with batching (300 without). Use when order matters, accept the throughput limit.
  • RabbitMQ: single broker node can handle ~50,000–100,000 messages/sec for small messages. Supports complex routing (direct, fanout, topic, headers exchanges), priority queues, TTL, and dead-letter exchanges natively.
Design rule of thumb: Whenever a web request triggers work that takes more than ~100 ms — sending email, resizing images, charging a card, generating a PDF — push that work onto a queue and return a 202 Accepted to the caller immediately. This keeps your API latency tight and your workers independently scalable.

When a Queue Is the Wrong Tool

Message queues are powerful but not universal. Avoid them when:

  • The caller needs a synchronous answer. A user clicking "checkout" needs to know if the payment succeeded now. A queue introduces latency and state complexity (you need to poll for a result or use WebSockets for the callback).
  • You need fan-out to many independent subscribers. A point-to-point queue delivers each message to one consumer. For broadcasting the same event to multiple services, use a pub/sub topic instead — that is the subject of the next lesson.
  • Message ordering across partitions is critical and non-negotiable. Standard queues cannot guarantee it. Consider a single-partition Kafka topic or a database-backed job queue instead.

The queue is one of the most powerful primitives in distributed-systems design. Mastering when and how to use it — and what its failure modes are — will pay dividends across every large-scale design you tackle.